Compare commits

...

23 Commits

Author SHA1 Message Date
David Peter
8e52fa2fab [ty] Explicit control over cycle entrypoint 2025-09-02 16:00:52 +02:00
David Peter
bbfcf6e111 [ty] __class_getitem__ is a classmethod (#20192)
## Summary

`__class_getitem__` is [implicitly a
classmethod](https://docs.python.org/3/reference/datamodel.html#object.__class_getitem__).

## Test Plan

Added regression test.
2025-09-01 11:22:19 +02:00
David Peter
5518c84ab3 [ty] Support __init_subclass__ (#20190)
## Summary

`__init_subclass__` is implicitly a classmethod.

closes https://github.com/astral-sh/ty/issues/1106

## Test Plan

Regression test
2025-09-01 10:16:28 +02:00
renovate[bot]
c71ce006c4 Update dependency ruff to v0.12.11 (#20184)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [ruff](https://docs.astral.sh/ruff)
([source](https://redirect.github.com/astral-sh/ruff),
[changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md))
| `==0.12.10` -> `==0.12.11` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.12.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.12.10/0.12.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>astral-sh/ruff (ruff)</summary>

###
[`v0.12.11`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#01211)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.12.10...0.12.11)

##### Preview features

- \[`airflow`] Extend `AIR311` and `AIR312` rules
([#&#8203;20082](https://redirect.github.com/astral-sh/ruff/pull/20082))
- \[`airflow`] Replace wrong path `airflow.io.storage` with
`airflow.io.store` (`AIR311`)
([#&#8203;20081](https://redirect.github.com/astral-sh/ruff/pull/20081))
- \[`flake8-async`] Implement
`blocking-http-call-httpx-in-async-function` (`ASYNC212`)
([#&#8203;20091](https://redirect.github.com/astral-sh/ruff/pull/20091))
- \[`flake8-logging-format`] Add auto-fix for f-string logging calls
(`G004`)
([#&#8203;19303](https://redirect.github.com/astral-sh/ruff/pull/19303))
- \[`flake8-use-pathlib`] Add autofix for `PTH211`
([#&#8203;20009](https://redirect.github.com/astral-sh/ruff/pull/20009))
- \[`flake8-use-pathlib`] Make `PTH100` fix unsafe because it can change
behavior
([#&#8203;20100](https://redirect.github.com/astral-sh/ruff/pull/20100))

##### Bug fixes

- \[`pyflakes`, `pylint`] Fix false positives caused by `__class__` cell
handling (`F841`, `PLE0117`)
([#&#8203;20048](https://redirect.github.com/astral-sh/ruff/pull/20048))
- \[`pyflakes`] Fix `allowed-unused-imports` matching for top-level
modules (`F401`)
([#&#8203;20115](https://redirect.github.com/astral-sh/ruff/pull/20115))
- \[`ruff`] Fix false positive for t-strings in `default-factory-kwarg`
(`RUF026`)
([#&#8203;20032](https://redirect.github.com/astral-sh/ruff/pull/20032))
- \[`ruff`] Preserve relative whitespace in multi-line expressions
(`RUF033`)
([#&#8203;19647](https://redirect.github.com/astral-sh/ruff/pull/19647))

##### Rule changes

- \[`ruff`] Handle empty t-strings in
`unnecessary-empty-iterable-within-deque-call` (`RUF037`)
([#&#8203;20045](https://redirect.github.com/astral-sh/ruff/pull/20045))

##### Documentation

- Fix incorrect `D413` links in docstrings convention FAQ
([#&#8203;20089](https://redirect.github.com/astral-sh/ruff/pull/20089))
- \[`flake8-use-pathlib`] Update links to the table showing the
correspondence between `os` and `pathlib`
([#&#8203;20103](https://redirect.github.com/astral-sh/ruff/pull/20103))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:51:28 +02:00
renovate[bot]
2f3bf03d82 Update rui314/setup-mold digest to 725a879 (#20181)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rui314/setup-mold](https://redirect.github.com/rui314/setup-mold) |
action | digest | `7344740` -> `725a879` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:51:00 +02:00
renovate[bot]
24a6d247f1 Update CodSpeedHQ/action action to v3.8.1 (#20183)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [CodSpeedHQ/action](https://redirect.github.com/CodSpeedHQ/action) |
action | patch | `v3.8.0` -> `v3.8.1` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>CodSpeedHQ/action (CodSpeedHQ/action)</summary>

###
[`v3.8.1`](https://redirect.github.com/CodSpeedHQ/action/releases/tag/v3.8.1)

[Compare
Source](https://redirect.github.com/CodSpeedHQ/action/compare/v3.8.0...v3.8.1)

##### What's Changed

##### <!-- 1 -->🐛 Bug Fixes

- Don't show error when libpython is not found by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias)

##### <!-- 2 -->🏗️ Refactor

- Improve conditional compilation in `get_pipe_open_options` by
[@&#8203;art049](https://redirect.github.com/art049) in
[#&#8203;100](https://redirect.github.com/CodSpeedHQ/runner/pull/100)

##### <!-- 7 -->⚙️ Internals

- Change log level to warn for venv\_compat error by
[@&#8203;not-matthias](https://redirect.github.com/not-matthias) in
[#&#8203;104](https://redirect.github.com/CodSpeedHQ/runner/pull/104)

**Full Changelog**:
<https://github.com/CodSpeedHQ/action/compare/v3.8.0...v3.8.1>
**Full Runner Changelog**:
<https://github.com/CodSpeedHQ/runner/blob/main/CHANGELOG.md>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:50:02 +02:00
renovate[bot]
88f7644551 Update cargo-bins/cargo-binstall action to v1.15.3 (#20182)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cargo-bins/cargo-binstall](https://redirect.github.com/cargo-bins/cargo-binstall)
| action | patch | `v1.15.1` -> `v1.15.3` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>cargo-bins/cargo-binstall (cargo-bins/cargo-binstall)</summary>

###
[`v1.15.3`](https://redirect.github.com/cargo-bins/cargo-binstall/releases/tag/v1.15.3)

[Compare
Source](https://redirect.github.com/cargo-bins/cargo-binstall/compare/v1.15.2...v1.15.3)

*Binstall is a tool to fetch and install Rust-based executables as
binaries. It aims to be a drop-in replacement for `cargo install` in
most cases. Install it today with `cargo install cargo-binstall`, from
the binaries below, or if you already have it, upgrade with `cargo
binstall cargo-binstall`.*

##### In this release:

- Upgrade (de)compression libraries
([#&#8203;2289](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2289))
- Fix release pipeline: stop building for x86\_64h-apple-darwin since it
is broken and is a rarely used tier 3 target, plus GHA no longer offers
x86\_64 mac
([#&#8203;2292](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2292))

###
[`v1.15.2`](https://redirect.github.com/cargo-bins/cargo-binstall/releases/tag/v1.15.2)

[Compare
Source](https://redirect.github.com/cargo-bins/cargo-binstall/compare/v1.15.1...v1.15.2)

*Binstall is a tool to fetch and install Rust-based executables as
binaries. It aims to be a drop-in replacement for `cargo install` in
most cases. Install it today with `cargo install cargo-binstall`, from
the binaries below, or if you already have it, upgrade with `cargo
binstall cargo-binstall`.*

##### In this release:

- Fix updating telemetry config
([#&#8203;2279](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2279)
[#&#8203;2287](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2287))

##### Other changes:

- Upgrade dependencies
([#&#8203;2278](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2278)
[#&#8203;2286](https://redirect.github.com/cargo-bins/cargo-binstall/issues/2286))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:49:31 +02:00
renovate[bot]
6be46033b9 Update Rust crate camino to v1.1.12 (#20185)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [camino](https://redirect.github.com/camino-rs/camino) |
workspace.dependencies | patch | `1.1.11` -> `1.1.12` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>camino-rs/camino (camino)</summary>

###
[`v1.1.12`](https://redirect.github.com/camino-rs/camino/blob/HEAD/CHANGELOG.md#1112---2025-08-26)

[Compare
Source](https://redirect.github.com/camino-rs/camino/compare/camino-1.1.11...camino-1.1.12)

##### Added

- `Utf8PathBuf::from_os_string` and `Utf8Path::from_os_str` conversions.
- `TryFrom<OsString> for Utf8PathBuf` and `TryFrom<&OsStr> for
&Utf8Path` conversions.

Thanks to [BenjaminBrienen](https://redirect.github.com/BenjaminBrienen)
for your first contribution!

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:48:52 +02:00
renovate[bot]
9e292f1522 Update Rust crate clap to v4.5.46 (#20186)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.45` -> `4.5.46` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.5.46`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4546---2025-08-26)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.45...v4.5.46)

##### Features

- Expose `StyledStr::push_str`

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:48:35 +02:00
renovate[bot]
74b60212dc Update Rust crate mimalloc to v0.1.48 (#20187)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mimalloc](https://redirect.github.com/purpleprotocol/mimalloc_rust) |
workspace.dependencies | patch | `0.1.47` -> `0.1.48` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>purpleprotocol/mimalloc_rust (mimalloc)</summary>

###
[`v0.1.48`](https://redirect.github.com/purpleprotocol/mimalloc_rust/releases/tag/v0.1.48):
Version 0.1.48

[Compare
Source](https://redirect.github.com/purpleprotocol/mimalloc_rust/compare/v0.1.47...v0.1.48)

##### Changes

- Mimalloc `v3` feature flag. (credits
[@&#8203;gschulze](https://redirect.github.com/gschulze)).

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuOTEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:48:22 +02:00
github-actions[bot]
bfe501b802 [ty] Sync vendored typeshed stubs (#20188)
Close and reopen this PR to trigger CI

---------

Co-authored-by: typeshedbot <>
2025-09-01 06:27:48 +01:00
Carl Meyer
6f2b874d6c [ty] improve cycle-detection coverage for apply_type_mapping (#20159)
## Summary

Thread visitors through the rest of `apply_type_mapping`: callable and
protocol types.

## Test Plan

Added mdtest that previously stack overflowed.
2025-08-29 16:20:07 -07:00
Carl Meyer
17dc2e4d80 [ty] don't assume that deferred type inference means deferred name resolution (#20160)
## Summary

We have the ability to defer type inference of some parts of
definitions, so as to allow us to create a type that may need to be
recursively referenced in those other parts of the definition.

We also have the ability to do type inference in a context where all
name resolution should be deferred (that is, names should be looked up
from all-reachable-definitions rather than from the location of use.)
This is used for all annotations in stubs, or if `from __future__ import
annotations` is active.

Previous to this PR, these two concepts were linked: deferred-inference
always implied deferred-name-resolution, though we also supported
deferred-name-resolution without deferred-inference, via
`DeferredExpressionState`.

For the upcoming `typing.TypeAlias` support, I will defer inference of
the entire RHS of the alias (so as to support cycles), but that doesn't
imply deferred name resolution; at runtime, the RHS of a name annotated
as `typing.TypeAlias` is executed eagerly.

So this PR fully de-couples the two concepts, instead explicitly setting
the `DeferredExpressionState` in those cases where we should defer name
resolution.

It also fixes a long-standing related bug, where we were deferring name
resolution of all names in class bases, if any of the class bases
contained a stringified annotation.

## Test Plan

Added test that failed before this PR.
2025-08-29 16:19:45 -07:00
Dylan
694e7ed52e Less confidently mark f-strings as empty when inferring truthiness (#20152)
When computing the boolean value of an f-string, we over-eagerly
interpreted some f-string interpolations as empty. In this PR we now
mark the truthiness of f-strings involving format specs, debug text, and
bytes literals as "unknown".

This will probably result in some false negatives, which may be further
refined (for example - there are probably many cases where
`is_not_empty_f_string` should be modified to return `true`), but for
now at least we should have fewer false positives.

Affected rules (may not be an exhaustive list):

- [unnecessary-empty-iterable-within-deque-call
(RUF037)](https://docs.astral.sh/ruff/rules/unnecessary-empty-iterable-within-deque-call/#unnecessary-empty-iterable-within-deque-call-ruf037)
- [falsy-dict-get-fallback
(RUF056)](https://docs.astral.sh/ruff/rules/falsy-dict-get-fallback/#falsy-dict-get-fallback-ruf056)
- [pytest-assert-always-false
(PT015)](https://docs.astral.sh/ruff/rules/pytest-assert-always-false/#pytest-assert-always-false-pt015)
- [expr-or-not-expr
(SIM221)](https://docs.astral.sh/ruff/rules/expr-or-not-expr/#expr-or-not-expr-sim221)
- [expr-or-true
(SIM222)](https://docs.astral.sh/ruff/rules/expr-or-true/#expr-or-true-sim222)
- [expr-and-false
(SIM223)](https://docs.astral.sh/ruff/rules/expr-and-false/#expr-and-false-sim223)

Closes #19935
2025-08-29 22:12:54 +00:00
Carl Meyer
fe953e5c5c [ty] skip a slow seed in fuzzer (#20161)
## Summary

Fuzzer seed 208 seems to be timing out all fuzzer runs on PRs today.
This has happened on multiple unrelated PRs, as well as on an initial
version of this PR that made a comment-only change in ty and didn't skip
any seeds, so the timeout appears to be consistent in CI, on ty main
branch, as of today, but it started happening due to some change in a
factor outside ty; not sure what.

I checked the code generated for seed 208 locally, and it takes about
30s to check on current ty main branch. This is slow for a fuzzer seed,
but shouldn't be slow enough to make it time out after 20min in CI (even
accounting for GH runners being slower than my laptop.)

I tried to bisect the slowness of checking that code locally, but I
didn't go back far enough to find the change that made it slow. In fact
it seems like it became significantly faster in the last few days (on an
older checkout I had to stop it after several minutes.) So whatever the
cause of the slowness, it's not a recent change in ty.

I don't want to rabbit-hole on this right now (fuzzer-discovered issues
are lower-priority than real-world-code issues), and need a working CI,
so skip this seed for now until we can investigate it.

## Test Plan

CI. This PR contains a no-op (comment) change in ty, so that the fuzz
test is triggered in CI and we can verify it now works (as well as
verify, on the previous commit, that the fuzzer job is timing out on
that seed, even with just a no-op change in ty.)
2025-08-29 13:59:16 -07:00
Alex Waygood
0bf5d2a204 Revert "[ty] Use invalid-assignment error code for invalid assignments to ClassVars" (#20158)
Reverts astral-sh/ruff#20156. As @sharkdp noted in his post-merge
review, there were several issues with that PR that I didn't spot before
merging — but I'm out for four days now, and would rather not leave
things in an inconsistent state for that long. I'll revisit this on
Wednesday.
2025-08-29 19:48:45 +01:00
Carl Meyer
8eb8d25565 [ty] add six ecosystem projects to good.txt (#20157)
## Summary

These projects all check successfully now.

(Pandas still takes 9s, as the comment in `bad.txt` said, but I don't
think this is slow enough to exclude it; mypy-primer overall still runs
in 4 minutes, faster than e.g. the test suite on Windows.)

## Test Plan

mypy-primer CI.
2025-08-29 11:37:29 -07:00
Alex Waygood
9b1b58a451 [ty] Use invalid-assignment error code for invalid assignments to ClassVars (#20156)
## Summary

This error is about assigning to attributes rather than reading
attributes, so I think `invalid-assignment` makes more sense than
`invalid-attribute-access`

## Test Plan

existing mdtests updated
2025-08-29 18:43:30 +01:00
Carl Meyer
fa7798ddd9 [ty] minor TypedDict fixes (#20146)
## Summary

In `is_disjoint_from_impl`, we should unpack type aliases before we
check `TypedDict`. This change probably doesn't have any visible effect
until we have a more discriminating implementation of disjointness for
`TypedDict`, but making the change now can avoid some confusion/bugs in
future.

In `type_ordering.rs`, we should order `TypedDict` near more similar
types, and leave Union/Intersection together at the end of the list.
This is not necessary for correctness, but it's more consistent and it
could have saved me some confusion trying to figure out why I was only
getting an unreachable panic when my code example included a `TypedDict`
type.

## Test Plan

None besides existing tests.
2025-08-29 09:46:48 -07:00
Carl Meyer
8223fea062 [ty] ensure union normalization really normalizes (#20147)
## Summary

Now that we have `Type::TypeAlias`, which can wrap a union, and the
possibility of unions including non-unpacked type aliases (which is
necessary to support recursive type aliases), we can no longer assume in
`UnionType::normalized_impl` that normalizing each element of an
existing union will result in a set of elements that we can order and
then place raw into `UnionType` to create a normalized union. It's now
possible for those elements to themselves include union types (unpacked
from an alias). So instead, we need to feed those elements into the full
`UnionBuilder` (with alias-unpacking turned on) to flatten/normalize
them, and then order them.

## Test Plan

Added mdtest.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-08-29 09:02:35 -07:00
Eric Jolibois
5a608f7366 [ty] typecheck dict methods for TypedDict (#19874)
## Summary

Typecheck `get()`, `setdefault()`, `pop()` for `TypedDict`

```py
from typing import TypedDict
from typing_extensions import NotRequired

class Employee(TypedDict):
    name: str
    department: NotRequired[str]

emp = Employee(name="Alice", department="Engineering")

emp.get("name")
emp.get("departmen", "Unknown")
emp.pop("department")
emp.pop("name")
```

<img width="838" height="529" alt="Screenshot 2025-08-12 at 11 42 12"
src="https://github.com/user-attachments/assets/77ce150a-223c-4931-b914-551095d8a3a6"
/>


part of https://github.com/astral-sh/ty/issues/154

## Test Plan

Updated Markdown tests

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-08-29 16:25:03 +02:00
Hans
c2d7c673ca [pyupgrade] Add fix safety section to docs (UP029) (#17490)
## Summary

Add `fix safety` section to `UP029: unnecessary_builtin_import.rs`, for
#15584
2025-08-29 13:55:19 +00:00
Brent Westbrook
8a6db4f257 Show fixes by default (#19919)
## Summary

This PR fixes #7352 by exposing the `show_fix_diff` option used in our
snapshot tests in the CLI. As the issue suggests, we plan to make this
the default output format in the future, so this is added to the `full`
output format in preview for now.

This turned out to be pretty straightforward. I just used our existing
`Applicability` settings to determine whether or not to print the diff.

The snapshot differences are because we now set
`Applicability::DisplayOnly` for our snapshot tests. This
`Applicability` is also used to determine whether or not the fix icon
(`[*]`) is rendered, so this is now shown for display-only fixes in our
snapshots. This was already the case previously, but we were only
setting `Applicability::Unsafe` in these tests and ignoring the
`Applicability` when rendering fix diffs. CLI users can't enable
display-only fixes, so this is only a test change for now, but this
should work smoothly if we decide to expose a `--display-only-fixes`
flag or similar in the future.

I also deleted the `PrinterFlags::SHOW_FIX_DIFF` flag. This was
completely unused before, and it seemed less confusing just to delete it
than to enable it in the right place and check it along with the
`OutputFormat` and `preview`.

## Test Plan

I only added one CLI test for now. I'm kind of assuming that we have
decent coverage of the cases where this shouldn't be firing, especially
the `output_format` CLI test, which shows that this definitely doesn't
affect non-preview `full` output. I'm happy to add more tests with
different combinations of options, if we're worried about any in
particular. I did try `--diff` and `--preview` and a few other
combinations manually.

And here's a screenshot using our trusty UP049 example from the design
discussion confirming that all the colors and other formatting still
look as expected:

<img width="786" height="629" alt="image"
src="https://github.com/user-attachments/assets/94e408bc-af7b-4573-b546-a5ceac2620f2"
/>

And one with an unsafe fix to see the footer:

<img width="782" height="367" alt="image"
src="https://github.com/user-attachments/assets/bbb29e47-310b-4293-b2c2-cc7aee3baff4"
/>


## Related issues and PR
- https://github.com/astral-sh/ruff/issues/7352
- https://github.com/astral-sh/ruff/pull/12595
- https://github.com/astral-sh/ruff/issues/12598
- https://github.com/astral-sh/ruff/issues/12599
- https://github.com/astral-sh/ruff/issues/12600

I think we could probably close all of these issues now. I think we've
either resolved or avoided most of them, and if we encounter them again
with the new output format, it would probably make sense to open new
ones anyway.
2025-08-29 09:53:05 -04:00
57 changed files with 1301 additions and 624 deletions

View File

@@ -250,7 +250,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
@@ -308,7 +308,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
@@ -393,7 +393,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Build"
run: cargo build --release --locked
@@ -418,7 +418,7 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Build tests"
shell: bash
env:
@@ -441,7 +441,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@0dca8cf8dfb40cb77a29cece06933ce674674523 # v1.15.1
uses: cargo-bins/cargo-binstall@2bb61346d075e720d4c3da92f23b6d612d5a7543 # v1.15.3
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -694,7 +694,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@0dca8cf8dfb40cb77a29cece06933ce674674523 # v1.15.1
- uses: cargo-bins/cargo-binstall@2bb61346d075e720d4c3da92f23b6d612d5a7543 # v1.15.3
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -923,7 +923,7 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c # v3.8.1
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
@@ -956,7 +956,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c # v3.8.1
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -38,7 +38,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: Build ruff
# A debug build means the script runs slower once it gets started,

26
Cargo.lock generated
View File

@@ -322,9 +322,9 @@ dependencies = [
[[package]]
name = "camino"
version = "1.1.11"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0"
checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5"
dependencies = [
"serde",
]
@@ -408,9 +408,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.45"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
dependencies = [
"clap_builder",
"clap_derive",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.44"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
dependencies = [
"anstream",
"anstyle",
@@ -955,7 +955,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -1035,7 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -1795,9 +1795,9 @@ dependencies = [
[[package]]
name = "libmimalloc-sys"
version = "0.1.43"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d"
checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870"
dependencies = [
"cc",
"libc",
@@ -1949,9 +1949,9 @@ dependencies = [
[[package]]
name = "mimalloc"
version = "0.1.47"
version = "0.1.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40"
checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8"
dependencies = [
"libmimalloc-sys",
]
@@ -3434,7 +3434,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]

View File

@@ -30,8 +30,6 @@ bitflags! {
const SHOW_VIOLATIONS = 1 << 0;
/// Whether to show a summary of the fixed violations when emitting diagnostics.
const SHOW_FIX_SUMMARY = 1 << 1;
/// Whether to show a diff of each fixed violation when emitting diagnostics.
const SHOW_FIX_DIFF = 1 << 2;
}
}
@@ -260,9 +258,9 @@ impl Printer {
OutputFormat::Concise | OutputFormat::Full => {
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
.with_show_fix_diff(self.format == OutputFormat::Full && preview)
.with_show_source(self.format == OutputFormat::Full)
.with_unsafe_fixes(self.unsafe_fixes)
.with_fix_applicability(self.unsafe_fixes.required_applicability())
.with_preview(preview)
.emit(writer, &diagnostics.inner, &context)?;
@@ -464,7 +462,7 @@ impl Printer {
TextEmitter::default()
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_show_source(preview)
.with_unsafe_fixes(self.unsafe_fixes)
.with_fix_applicability(self.unsafe_fixes.required_applicability())
.emit(writer, &diagnostics.inner, &context)?;
}
writer.flush()?;

View File

@@ -5830,3 +5830,33 @@ nested_optional: Optional[Optional[Optional[str]]] = None
",
);
}
#[test]
fn show_fixes_in_full_output_with_preview_enabled() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--no-cache", "--output-format", "full"])
.args(["--select", "F401"])
.arg("--preview")
.arg("-")
.pass_stdin("import math"),
@r"
success: false
exit_code: 1
----- stdout -----
F401 [*] `math` imported but unused
--> -:1:8
|
1 | import math
| ^^^^
|
help: Remove unused import: `math`
- import math
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
",
);
}

View File

@@ -349,6 +349,13 @@ impl Diagnostic {
self.fix().is_some()
}
/// Returns `true` if the diagnostic is [`fixable`](Diagnostic::fixable) and applies at the
/// configured applicability level.
pub fn has_applicable_fix(&self, config: &DisplayDiagnosticConfig) -> bool {
self.fix()
.is_some_and(|fix| fix.applies(config.fix_applicability))
}
/// Returns the offset of the parent statement for this diagnostic if it exists.
///
/// This is primarily used for checking noqa/secondary code suppressions.

View File

@@ -254,9 +254,7 @@ impl<'a> ResolvedDiagnostic<'a> {
id,
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: diag
.fix()
.is_some_and(|fix| fix.applies(config.fix_applicability)),
is_fixable: diag.has_applicable_fix(config),
}
}

View File

@@ -77,11 +77,9 @@ impl<'a> ConciseRenderer<'a> {
)?;
}
if self.config.show_fix_status {
if let Some(fix) = diag.fix() {
// Do not display an indicator for inapplicable fixes
if fix.applies(self.config.fix_applicability) {
write!(f, "[{fix}] ", fix = fmt_styled("*", stylesheet.separator))?;
}
// Do not display an indicator for inapplicable fixes
if diag.has_applicable_fix(self.config) {
write!(f, "[{fix}] ", fix = fmt_styled("*", stylesheet.separator))?;
}
}
} else {

View File

@@ -58,7 +58,7 @@ impl<'a> FullRenderer<'a> {
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
}
if self.config.show_fix_diff {
if self.config.show_fix_diff && diag.has_applicable_fix(self.config) {
if let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver) {
write!(f, "{diff}")?;
}
@@ -697,6 +697,8 @@ print()
fn notebook_output_with_diff() {
let (mut env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
env.show_fix_diff(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
@@ -726,7 +728,7 @@ print()
2 |
3 | print('hello world')
error[unused-variable]: Local variable `x` is assigned to but never used
error[unused-variable][*]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
@@ -749,6 +751,7 @@ print()
fn notebook_output_with_diff_spanning_cells() {
let (mut env, mut diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
env.show_fix_diff(true);
env.fix_applicability(Applicability::DisplayOnly);
// Move all of the edits from the later diagnostics to the first diagnostic to simulate a
// single diagnostic with edits in different cells.
@@ -761,7 +764,7 @@ print()
*fix = Fix::unsafe_edits(edits.remove(0), edits);
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[unused-import]: `os` imported but unused
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
@@ -924,6 +927,7 @@ line 10
env.add("example.py", contents);
env.format(DiagnosticFormat::Full);
env.show_fix_diff(true);
env.fix_applicability(Applicability::DisplayOnly);
let mut diagnostic = env.err().primary("example.py", "3", "3", "label").build();
diagnostic.help("Start of diff:");
@@ -936,7 +940,7 @@ line 10
)));
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
error[test-diagnostic][*]: main diagnostic message
--> example.py:3:1
|
1 | line 1

View File

@@ -23,3 +23,11 @@ def test_error():
assert list([])
assert set(set())
assert tuple("")
# https://github.com/astral-sh/ruff/issues/19935
def test_all_ok():
assert f"{b""}"
assert f"{""=}"
assert f"{""!a}"
assert f"{""!r}"
assert f"{"":1}"

View File

@@ -110,3 +110,9 @@ deque(t"{""}") # OK
# https://github.com/astral-sh/ruff/issues/20050
deque(f"{""}") # RUF037
deque(f"{b""}")
deque(f"{""=}")
deque(f"{""!a}")
deque(f"{""!r}")
deque(f"{"":1}")

View File

@@ -3,9 +3,9 @@ use std::io::Write;
use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics,
};
use ruff_diagnostics::Applicability;
use crate::message::{Emitter, EmitterContext};
use crate::settings::types::UnsafeFixes;
pub struct TextEmitter {
config: DisplayDiagnosticConfig,
@@ -46,10 +46,8 @@ impl TextEmitter {
}
#[must_use]
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
self.config = self
.config
.fix_applicability(unsafe_fixes.required_applicability());
pub fn with_fix_applicability(mut self, applicability: Applicability) -> Self {
self.config = self.config.fix_applicability(applicability);
self
}
@@ -86,13 +84,13 @@ impl Emitter for TextEmitter {
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use ruff_diagnostics::Applicability;
use crate::message::TextEmitter;
use crate::message::tests::{
capture_emitter_notebook_output, capture_emitter_output, create_diagnostics,
create_notebook_diagnostics, create_syntax_error_diagnostics,
};
use crate::settings::types::UnsafeFixes;
#[test]
fn default() {
@@ -117,7 +115,7 @@ mod tests {
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled);
.with_fix_applicability(Applicability::Unsafe);
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(content);
@@ -128,7 +126,7 @@ mod tests {
let mut emitter = TextEmitter::default()
.with_show_fix_status(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled);
.with_fix_applicability(Applicability::Unsafe);
let (messages, notebook_indexes) = create_notebook_diagnostics();
let content = capture_emitter_notebook_output(&mut emitter, &messages, &notebook_indexes);

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/eradicate/mod.rs
---
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:1:1
|
1 | #import os
@@ -16,7 +16,7 @@ help: Remove commented-out code
3 | a = 4
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:2:1
|
1 | #import os
@@ -33,7 +33,7 @@ help: Remove commented-out code
4 | #foo(1, 2, 3)
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:3:1
|
1 | #import os
@@ -52,7 +52,7 @@ help: Remove commented-out code
5 |
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:5:1
|
3 | #a = 3
@@ -72,7 +72,7 @@ help: Remove commented-out code
7 | content = 1 # print('hello')
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:13:5
|
11 | # This is a real comment.
@@ -91,7 +91,7 @@ help: Remove commented-out code
15 | #import os # noqa: ERA001
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:21:5
|
19 | class A():
@@ -109,7 +109,7 @@ help: Remove commented-out code
23 | dictionary = {
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:26:5
|
24 | dictionary = {
@@ -129,7 +129,7 @@ help: Remove commented-out code
28 |
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:27:5
|
25 | # "key1": 123, # noqa: ERA001
@@ -148,7 +148,7 @@ help: Remove commented-out code
29 | #import os # noqa
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:32:1
|
30 | #import os # noqa
@@ -168,7 +168,7 @@ help: Remove commented-out code
34 | # try: print()
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:33:1
|
32 | # case 1:
@@ -187,7 +187,7 @@ help: Remove commented-out code
35 | # except:
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:34:1
|
32 | # case 1:
@@ -207,7 +207,7 @@ help: Remove commented-out code
36 | # except Foo:
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:35:1
|
33 | # try:
@@ -227,7 +227,7 @@ help: Remove commented-out code
37 | # except Exception as e: print(e)
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:36:1
|
34 | # try: # with comment
@@ -247,7 +247,7 @@ help: Remove commented-out code
38 |
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:37:1
|
35 | # try: print()
@@ -266,7 +266,7 @@ help: Remove commented-out code
39 |
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:38:1
|
36 | # except:
@@ -284,7 +284,7 @@ help: Remove commented-out code
40 | # Script tag without an opening tag (Error)
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:44:1
|
43 | # requires-python = ">=3.11"
@@ -303,7 +303,7 @@ help: Remove commented-out code
46 | # ]
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:47:1
|
45 | # "requests<3",
@@ -322,7 +322,7 @@ help: Remove commented-out code
49 | # Script tag (OK)
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:75:1
|
73 | # /// script
@@ -342,7 +342,7 @@ help: Remove commented-out code
77 | # ]
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> ERA001.py:78:1
|
76 | # "requests<3",

View File

@@ -277,7 +277,7 @@ help: Use `Self` as return type
334 | def __new__(cls: type[Generic1]) -> Generic1: ...
note: This is an unsafe fix and may change runtime behavior
PYI034 `__new__` methods in classes like `Generic1` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic1` usually return `self` at runtime
--> PYI034.py:334:9
|
333 | class Generic1[T](list):
@@ -296,7 +296,7 @@ help: Use `Self` as return type
337 |
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic1` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic1` usually return `self` at runtime
--> PYI034.py:335:9
|
333 | class Generic1[T](list):
@@ -315,7 +315,7 @@ help: Use `Self` as return type
338 | ### Correctness of typevar-likes are not verified.
note: This is a display-only fix and is likely to be incorrect
PYI034 `__new__` methods in classes like `Generic2` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic2` usually return `self` at runtime
--> PYI034.py:345:9
|
344 | class Generic2(Generic[T]):
@@ -334,7 +334,7 @@ help: Use `Self` as return type
348 | class Generic3(tuple[*Ts]):
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic2` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic2` usually return `self` at runtime
--> PYI034.py:346:9
|
344 | class Generic2(Generic[T]):
@@ -355,7 +355,7 @@ help: Use `Self` as return type
349 | def __new__(cls: type[Generic3]) -> Generic3: ...
note: This is a display-only fix and is likely to be incorrect
PYI034 `__new__` methods in classes like `Generic3` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic3` usually return `self` at runtime
--> PYI034.py:349:9
|
348 | class Generic3(tuple[*Ts]):
@@ -374,7 +374,7 @@ help: Use `Self` as return type
352 | class Generic4(collections.abc.Callable[P, ...]):
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic3` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic3` usually return `self` at runtime
--> PYI034.py:350:9
|
348 | class Generic3(tuple[*Ts]):
@@ -395,7 +395,7 @@ help: Use `Self` as return type
353 | def __new__(cls: type[Generic4]) -> Generic4: ...
note: This is a display-only fix and is likely to be incorrect
PYI034 `__new__` methods in classes like `Generic4` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic4` usually return `self` at runtime
--> PYI034.py:353:9
|
352 | class Generic4(collections.abc.Callable[P, ...]):
@@ -414,7 +414,7 @@ help: Use `Self` as return type
356 | from some_module import PotentialTypeVar
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic4` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic4` usually return `self` at runtime
--> PYI034.py:354:9
|
352 | class Generic4(collections.abc.Callable[P, ...]):

View File

@@ -258,7 +258,7 @@ help: Use `Self` as return type
228 | def __new__(cls: type[Generic1]) -> Generic1: ...
note: This is an unsafe fix and may change runtime behavior
PYI034 `__new__` methods in classes like `Generic1` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic1` usually return `self` at runtime
--> PYI034.pyi:228:9
|
227 | class Generic1[T](list):
@@ -277,7 +277,7 @@ help: Use `Self` as return type
231 |
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic1` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic1` usually return `self` at runtime
--> PYI034.pyi:229:9
|
227 | class Generic1[T](list):
@@ -296,7 +296,7 @@ help: Use `Self` as return type
232 | ### Correctness of typevar-likes are not verified.
note: This is a display-only fix and is likely to be incorrect
PYI034 `__new__` methods in classes like `Generic2` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic2` usually return `self` at runtime
--> PYI034.pyi:239:9
|
238 | class Generic2(Generic[T]):
@@ -315,7 +315,7 @@ help: Use `Self` as return type
242 | class Generic3(tuple[*Ts]):
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic2` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic2` usually return `self` at runtime
--> PYI034.pyi:240:9
|
238 | class Generic2(Generic[T]):
@@ -336,7 +336,7 @@ help: Use `Self` as return type
243 | def __new__(cls: type[Generic3]) -> Generic3: ...
note: This is a display-only fix and is likely to be incorrect
PYI034 `__new__` methods in classes like `Generic3` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic3` usually return `self` at runtime
--> PYI034.pyi:243:9
|
242 | class Generic3(tuple[*Ts]):
@@ -355,7 +355,7 @@ help: Use `Self` as return type
246 | class Generic4(collections.abc.Callable[P, ...]):
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic3` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic3` usually return `self` at runtime
--> PYI034.pyi:244:9
|
242 | class Generic3(tuple[*Ts]):
@@ -376,7 +376,7 @@ help: Use `Self` as return type
247 | def __new__(cls: type[Generic4]) -> Generic4: ...
note: This is a display-only fix and is likely to be incorrect
PYI034 `__new__` methods in classes like `Generic4` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic4` usually return `self` at runtime
--> PYI034.pyi:247:9
|
246 | class Generic4(collections.abc.Callable[P, ...]):
@@ -395,7 +395,7 @@ help: Use `Self` as return type
250 | from some_module import PotentialTypeVar
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic4` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic4` usually return `self` at runtime
--> PYI034.pyi:248:9
|
246 | class Generic4(collections.abc.Callable[P, ...]):
@@ -416,7 +416,7 @@ help: Use `Self` as return type
251 |
note: This is a display-only fix and is likely to be incorrect
PYI034 `__new__` methods in classes like `Generic5` usually return `self` at runtime
PYI034 [*] `__new__` methods in classes like `Generic5` usually return `self` at runtime
--> PYI034.pyi:253:9
|
252 | class Generic5(list[PotentialTypeVar]):
@@ -433,7 +433,7 @@ help: Use `Self` as return type
254 | def __enter__(self: Generic5) -> Generic5: ...
note: This is a display-only fix and is likely to be incorrect
PYI034 `__enter__` methods in classes like `Generic5` usually return `self` at runtime
PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` at runtime
--> PYI034.pyi:254:9
|
252 | class Generic5(list[PotentialTypeVar]):

View File

@@ -182,4 +182,6 @@ PT015 Assertion always fails, replace with `pytest.fail()`
24 | assert set(set())
25 | assert tuple("")
| ^^^^^^^^^^^^^^^^
26 |
27 | # https://github.com/astral-sh/ruff/issues/19935
|

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
---
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:3:16
|
1 | # Errors
@@ -21,7 +21,7 @@ help: Remove default argument
6 | def test_foo(a: int=1): ...
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:4:18
|
3 | def test_foo(a=1): ...
@@ -41,7 +41,7 @@ help: Remove default argument
7 | def test_foo(a: int = 1): ...
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:5:19
|
3 | def test_foo(a=1): ...
@@ -62,7 +62,7 @@ help: Remove default argument
8 | def test_foo(a: (int) = 1): ...
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:6:21
|
4 | def test_foo(a = 1): ...
@@ -83,7 +83,7 @@ help: Remove default argument
9 | def test_foo(a: int = (1)): ...
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:7:23
|
5 | def test_foo(a = (1)): ...
@@ -104,7 +104,7 @@ help: Remove default argument
10 | def test_foo(a: (int) = (1)): ...
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:8:25
|
6 | def test_foo(a: int=1): ...
@@ -125,7 +125,7 @@ help: Remove default argument
11 | def test_foo(a=1, /, b=2, *, c=3): ...
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:9:24
|
7 | def test_foo(a: int = 1): ...
@@ -146,7 +146,7 @@ help: Remove default argument
12 |
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:10:26
|
8 | def test_foo(a: (int) = 1): ...
@@ -166,7 +166,7 @@ help: Remove default argument
13 |
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> PT028.py:11:16
|
9 | def test_foo(a: int = (1)): ...
@@ -185,7 +185,7 @@ help: Remove default argument
14 | # No errors
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `b` has default argument
PT028 [*] Test function parameter `b` has default argument
--> PT028.py:11:24
|
9 | def test_foo(a: int = (1)): ...
@@ -204,7 +204,7 @@ help: Remove default argument
14 | # No errors
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `c` has default argument
PT028 [*] Test function parameter `c` has default argument
--> PT028.py:11:32
|
9 | def test_foo(a: int = (1)): ...

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
---
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> is_pytest_test.py:3:27
|
1 | # Errors
@@ -20,7 +20,7 @@ help: Remove default argument
6 | class TestClass:
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> is_pytest_test.py:4:27
|
3 | def test_this_is_a_test(a=1): ...
@@ -40,7 +40,7 @@ help: Remove default argument
7 | def test_this_too_is_a_test(self, a=1): ...
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> is_pytest_test.py:7:41
|
6 | class TestClass:
@@ -59,7 +59,7 @@ help: Remove default argument
10 |
note: This is a display-only fix and is likely to be incorrect
PT028 Test function parameter `a` has default argument
PT028 [*] Test function parameter `a` has default argument
--> is_pytest_test.py:8:37
|
6 | class TestClass:

View File

@@ -120,7 +120,7 @@ help: Rewrite `f` as a `def`
61 | class Scope:
note: This is an unsafe fix and may change runtime behavior
E731 Do not assign a `lambda` expression, use a `def`
E731 [*] Do not assign a `lambda` expression, use a `def`
--> E731.py:73:9
|
71 | x: Callable[[int], int]
@@ -142,7 +142,7 @@ help: Rewrite `x` as a `def`
77 | return x
note: This is a display-only fix and is likely to be incorrect
E731 Do not assign a `lambda` expression, use a `def`
E731 [*] Do not assign a `lambda` expression, use a `def`
--> E731.py:75:9
|
73 | x = lambda: 1

View File

@@ -27,6 +27,21 @@ use crate::{AlwaysFixableViolation, Fix};
/// str(1)
/// ```
///
/// ## Fix safety
/// This fix is marked as unsafe because removing the import
/// may change program behavior. For example, in the following
/// situation:
///
/// ```python
/// def str(x):
/// return x
///
///
/// from builtins import str
///
/// str(1) # `"1"` with the import, `1` without
/// ```
///
/// ## References
/// - [Python documentation: The Python Standard Library](https://docs.python.org/3/library/index.html)
#[derive(ViolationMetadata)]

View File

@@ -262,7 +262,7 @@ UP049 Generic class uses private type parameters
|
help: Rename type parameter to remove leading underscores
UP049 Generic class uses private type parameters
UP049 [*] Generic class uses private type parameters
--> UP049_1.py:71:9
|
71 | class C[_T]:
@@ -295,7 +295,7 @@ help: Rename type parameter to remove leading underscores
82 | class C[_T]:
note: This is a display-only fix and is likely to be incorrect
UP049 Generic class uses private type parameters
UP049 [*] Generic class uses private type parameters
--> UP049_1.py:82:9
|
82 | class C[_T]:

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB116 Replace `oct` call with `f"{num:o}"`
FURB116 [*] Replace `oct` call with `f"{num:o}"`
--> FURB116.py:9:7
|
7 | return num
@@ -22,7 +22,7 @@ help: Replace with `f"{num:o}"`
12 |
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `hex` call with `f"{num:x}"`
FURB116 [*] Replace `hex` call with `f"{num:x}"`
--> FURB116.py:10:7
|
9 | print(oct(num)[2:]) # FURB116
@@ -41,7 +41,7 @@ help: Replace with `f"{num:x}"`
13 | print(oct(1337)[2:]) # FURB116
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `bin` call with `f"{num:b}"`
FURB116 [*] Replace `bin` call with `f"{num:b}"`
--> FURB116.py:11:7
|
9 | print(oct(num)[2:]) # FURB116
@@ -162,7 +162,7 @@ FURB116 Replace `bin` call with f-string
|
help: Replace with f-string
FURB116 Replace `bin` call with `f"{d:b}"`
FURB116 [*] Replace `bin` call with `f"{d:b}"`
--> FURB116.py:32:7
|
30 | d = datetime.datetime.now(tz=datetime.UTC)
@@ -183,7 +183,7 @@ help: Replace with `f"{d:b}"`
35 |
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `bin` call with `f"{len("xyz").numerator:b}"`
FURB116 [*] Replace `bin` call with `f"{len("xyz").numerator:b}"`
--> FURB116.py:34:7
|
32 | print(bin(d)[2:])
@@ -204,7 +204,7 @@ help: Replace with `f"{len("xyz").numerator:b}"`
37 | print(bin({0: 1}[0].numerator)[2:])
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"`
FURB116 [*] Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"`
--> FURB116.py:37:7
|
36 | # autofix is display-only
@@ -224,7 +224,7 @@ help: Replace with `f"{ {0: 1}[0].numerator:b}"`
40 | print(hex(sys
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `bin` call with `f"{ord("\\").numerator:b}"`
FURB116 [*] Replace `bin` call with `f"{ord("\\").numerator:b}"`
--> FURB116.py:39:7
|
37 | print(bin({0: 1}[0].numerator)[2:])
@@ -245,7 +245,7 @@ help: Replace with `f"{ord("\\").numerator:b}"`
42 |
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `hex` call with f-string
FURB116 [*] Replace `hex` call with f-string
--> FURB116.py:40:7
|
38 | # no autofix for Python 3.11 and earlier, as it introduces a syntax error
@@ -270,7 +270,7 @@ help: Replace with f-string
44 | print(bin(-1)[2:])
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `bin` call with `f"{-1:b}"`
FURB116 [*] Replace `bin` call with `f"{-1:b}"`
--> FURB116.py:44:7
|
43 | # for negatives numbers autofix is display-only

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB116 Replace `oct` call with `f"{num:o}"`
FURB116 [*] Replace `oct` call with `f"{num:o}"`
--> FURB116.py:9:7
|
7 | return num
@@ -22,7 +22,7 @@ help: Replace with `f"{num:o}"`
12 |
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `hex` call with `f"{num:x}"`
FURB116 [*] Replace `hex` call with `f"{num:x}"`
--> FURB116.py:10:7
|
9 | print(oct(num)[2:]) # FURB116
@@ -41,7 +41,7 @@ help: Replace with `f"{num:x}"`
13 | print(oct(1337)[2:]) # FURB116
note: This is a display-only fix and is likely to be incorrect
FURB116 Replace `bin` call with `f"{num:b}"`
FURB116 [*] Replace `bin` call with `f"{num:b}"`
--> FURB116.py:11:7
|
9 | print(oct(num)[2:]) # FURB116
@@ -162,7 +162,7 @@ FURB116 Replace `bin` call with f-string
|
help: Replace with f-string
FURB116 Replace `bin` call with `f"{d:b}"`
FURB116 [*] Replace `bin` call with `f"{d:b}"`
--> FURB116.py:32:7
|
30 | d = datetime.datetime.now(tz=datetime.UTC)
@@ -195,7 +195,7 @@ FURB116 Replace `bin` call with f-string
|
help: Replace with f-string
FURB116 Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"`
FURB116 [*] Replace `bin` call with `f"{ {0: 1}[0].numerator:b}"`
--> FURB116.py:37:7
|
36 | # autofix is display-only
@@ -241,7 +241,7 @@ FURB116 Replace `hex` call with f-string
|
help: Replace with f-string
FURB116 Replace `bin` call with `f"{-1:b}"`
FURB116 [*] Replace `bin` call with `f"{-1:b}"`
--> FURB116.py:44:7
|
43 | # for negatives numbers autofix is display-only

View File

@@ -396,6 +396,8 @@ RUF037 [*] Unnecessary empty iterable within a deque call
111 | # https://github.com/astral-sh/ruff/issues/20050
112 | deque(f"{""}") # RUF037
| ^^^^^^^^^^^^^^
113 |
114 | deque(f"{b""}")
|
help: Replace with `deque()`
109 | deque(t"{""}") # OK
@@ -403,3 +405,6 @@ help: Replace with `deque()`
111 | # https://github.com/astral-sh/ruff/issues/20050
- deque(f"{""}") # RUF037
112 + deque() # RUF037
113 |
114 | deque(f"{b""}")
115 | deque(f"{""=}")

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> RUF100_5.py:7:5
|
5 | # "key1": 123, # noqa: ERA001
@@ -20,7 +20,7 @@ help: Remove commented-out code
9 |
note: This is a display-only fix and is likely to be incorrect
ERA001 Found commented-out code
ERA001 [*] Found commented-out code
--> RUF100_5.py:11:1
|
11 | #import os # noqa: E501

View File

@@ -447,7 +447,7 @@ pub(crate) fn print_jupyter_messages(
.with_show_fix_status(true)
.with_show_fix_diff(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled)
.with_fix_applicability(Applicability::DisplayOnly)
.emit(
&mut output,
diagnostics,
@@ -468,7 +468,7 @@ pub(crate) fn print_messages(diagnostics: &[Diagnostic]) -> String {
.with_show_fix_status(true)
.with_show_fix_diff(true)
.with_show_source(true)
.with_unsafe_fixes(UnsafeFixes::Enabled)
.with_fix_applicability(Applicability::DisplayOnly)
.emit(
&mut output,
diagnostics,

View File

@@ -1410,31 +1410,38 @@ pub fn is_empty_f_string(expr: &ast::ExprFString) -> bool {
fn inner(expr: &Expr) -> bool {
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.is_empty(),
// Confusingly, `bool(f"{b""}") == True` even though
// `bool(b"") == False`. This is because `f"{b""}"`
// evaluates as the string `'b""'` of length 3.
Expr::BytesLiteral(_) => false,
Expr::FString(ast::ExprFString { value, .. }) => {
value
.elements()
.all(|f_string_element| match f_string_element {
InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement { value, .. },
) => value.is_empty(),
InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
expression,
..
}) => inner(expression),
})
is_empty_interpolated_elements(value.elements())
}
_ => false,
}
}
fn is_empty_interpolated_elements<'a>(
mut elements: impl Iterator<Item = &'a InterpolatedStringElement>,
) -> bool {
elements.all(|element| match element {
InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
value,
..
}) => value.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => {
f_string.debug_text.is_none()
&& f_string.conversion.is_none()
&& f_string.format_spec.is_none()
&& inner(&f_string.expression)
}
})
}
expr.value.iter().all(|part| match part {
ast::FStringPart::Literal(string_literal) => string_literal.is_empty(),
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
InterpolatedStringElement::Literal(string_literal) => string_literal.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
})
is_empty_interpolated_elements(f_string.elements.iter())
}
})
}

View File

@@ -1411,7 +1411,7 @@ quux.<CURSOR>
__getstate__ :: bound method Quux.__getstate__() -> object
__hash__ :: bound method Quux.__hash__() -> int
__init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: bound method Quux.__new__() -> Quux
@@ -1456,7 +1456,7 @@ quux.b<CURSOR>
__getstate__ :: bound method Quux.__getstate__() -> object
__hash__ :: bound method Quux.__hash__() -> int
__init__ :: bound method Quux.__init__() -> Unknown
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: bound method Quux.__new__() -> Quux
@@ -1506,7 +1506,7 @@ C.<CURSOR>
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: def __init__(self) -> None
__init_subclass__ :: def __init_subclass__(cls) -> None
__init_subclass__ :: bound method <class 'C'>.__init_subclass__() -> None
__instancecheck__ :: bound method <class 'C'>.__instancecheck__(instance: Any, /) -> bool
__itemsize__ :: int
__module__ :: str
@@ -1575,7 +1575,7 @@ Meta.<CURSOR>
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: Overload[(self, o: object, /) -> None, (self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None]
__init_subclass__ :: def __init_subclass__(cls) -> None
__init_subclass__ :: bound method <class 'Meta'>.__init_subclass__() -> None
__instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool
__itemsize__ :: int
__module__ :: str
@@ -1682,7 +1682,7 @@ Quux.<CURSOR>
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: def __init__(self) -> Unknown
__init_subclass__ :: def __init_subclass__(cls) -> None
__init_subclass__ :: bound method <class 'Quux'>.__init_subclass__() -> None
__instancecheck__ :: bound method <class 'Quux'>.__instancecheck__(instance: Any, /) -> bool
__itemsize__ :: int
__module__ :: str
@@ -1756,7 +1756,7 @@ Answer.<CURSOR>
__getstate__ :: def __getstate__(self) -> object
__hash__ :: def __hash__(self) -> int
__init__ :: def __init__(self) -> None
__init_subclass__ :: def __init_subclass__(cls) -> None
__init_subclass__ :: bound method <class 'Answer'>.__init_subclass__() -> None
__instancecheck__ :: bound method <class 'Answer'>.__instancecheck__(instance: Any, /) -> bool
__itemsize__ :: int
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__]

View File

@@ -462,6 +462,22 @@ reveal_type(C.f2(1)) # revealed: str
reveal_type(C().f2(1)) # revealed: str
```
### `__init_subclass__`
The [`__init_subclass__`] method is implicitly a classmethod:
```py
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.custom_attribute: int = 0
class Derived(Base):
pass
reveal_type(Derived.custom_attribute) # revealed: int
```
## `@staticmethod`
### Basic
@@ -571,3 +587,4 @@ reveal_type(C().f2(1)) # revealed: str
```
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
[`__init_subclass__`]: https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__

View File

@@ -0,0 +1,25 @@
# Class definitions
## Deferred resolution of bases
### Only the stringified name is deferred
If a class base contains a stringified name, only that name is deferred. Other names are resolved
normally.
```toml
[environment]
python-version = "3.12"
```
```py
A = int
class G[T]: ...
class C(A, G["B"]): ...
A = str
B = bytes
reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'int'>, <class 'G[bytes]'>, typing.Generic, <class 'object'>]
```

View File

@@ -120,6 +120,23 @@ def f(x: IntOrStr, y: str | bytes):
reveal_type(z) # revealed: (int & ~AlwaysFalsy) | str | bytes
```
## Multiple layers of union aliases
```py
class A: ...
class B: ...
class C: ...
class D: ...
type W = A | B
type X = C | D
type Y = W | X
from ty_extensions import is_equivalent_to, static_assert
static_assert(is_equivalent_to(Y, A | B | C | D))
```
## `TypeAliasType` properties
Two `TypeAliasType`s are distinct and disjoint, even if they refer to the same type
@@ -259,6 +276,17 @@ def h(x: Intersection[A, B]):
reveal_type(x) # revealed: tuple[B] | None
```
### Self-recursive callable type
```py
from typing import Callable
type C = Callable[[], C | None]
def _(x: C):
reveal_type(x) # revealed: () -> C | None
```
### Union inside generic
#### With old-style union

View File

@@ -19,6 +19,12 @@ class Identity:
reveal_type(Identity[0]) # revealed: str
```
`__class_getitem__` is implicitly a classmethod, so it can be called like this:
```py
reveal_type(Identity.__class_getitem__(0)) # revealed: str
```
## Class getitem union
```py

View File

@@ -450,19 +450,51 @@ class Person(TypedDict, total=False):
```py
from typing import TypedDict
from typing_extensions import NotRequired
class Person(TypedDict):
name: str
age: int | None
extra: NotRequired[str]
def _(p: Person) -> None:
reveal_type(p.keys()) # revealed: dict_keys[str, object]
reveal_type(p.values()) # revealed: dict_values[str, object]
reveal_type(p.setdefault("name", "Alice")) # revealed: @Todo(Support for `TypedDict`)
# `get()` returns the field type for required keys (no None union)
reveal_type(p.get("name")) # revealed: str
reveal_type(p.get("age")) # revealed: int | None
reveal_type(p.get("name")) # revealed: @Todo(Support for `TypedDict`)
reveal_type(p.get("name", "Unknown")) # revealed: @Todo(Support for `TypedDict`)
# It doesn't matter if a default is specified:
reveal_type(p.get("name", "default")) # revealed: str
reveal_type(p.get("age", 999)) # revealed: int | None
# `get()` can return `None` for non-required keys
reveal_type(p.get("extra")) # revealed: str | None
reveal_type(p.get("extra", "default")) # revealed: str
# The type of the default parameter can be anything:
reveal_type(p.get("extra", 0)) # revealed: str | Literal[0]
# We allow access to unknown keys (they could be set for a subtype of Person)
reveal_type(p.get("unknown")) # revealed: Unknown | None
reveal_type(p.get("unknown", "default")) # revealed: Unknown | Literal["default"]
# `pop()` only works on non-required fields
reveal_type(p.pop("extra")) # revealed: str
reveal_type(p.pop("extra", "fallback")) # revealed: str
# error: [invalid-argument-type] "Cannot pop required field 'name' from TypedDict `Person`"
reveal_type(p.pop("name")) # revealed: Unknown
# Similar to above, the default parameter can be of any type:
reveal_type(p.pop("extra", 0)) # revealed: str | Literal[0]
# `setdefault()` always returns the field type
reveal_type(p.setdefault("name", "Alice")) # revealed: str
reveal_type(p.setdefault("extra", "default")) # revealed: str
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?"
reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown
```
## Unlike normal classes

View File

@@ -1,9 +1,6 @@
Tanjun # too many iterations
altair # too many iterations (uses packaging)
antidote # hangs / slow (single threaded)
artigraph # cycle panics (value_type_)
arviz # too many iterations on versions of arviz newer than https://github.com/arviz-devs/arviz/commit/3205b82bb4d6097c31f7334d7ac51a6de37002d0
core # cycle panics (value_type_)
cpython # too many cycle iterations
graphql-core # stack overflow
hydpy # too many iterations
@@ -12,9 +9,6 @@ jax # too many iterations
mypy # too many iterations (self-recursive type alias)
nox # too many iterations (uses packaging)
packaging # too many iterations
pandas # slow (9s)
pandas-stubs # panics on versions of pandas-stubs newer than https://github.com/pandas-dev/pandas-stubs/commit/bf1221eb7ea0e582c30fe233d1f4f5713fce376b
# Panicked at crates/ty_python_semantic/src/types/type_ordering.rs:207:13 when checking `/tmp/mypy_primer/projects/pandas-stubs/tests/test_indexes.py`: `internal error: entered unreachable code: our type representation does not permit nested unions`
pandera # too many iterations
pip # vendors packaging, see above
pylint # cycle panics (self-recursive type alias)
@@ -25,4 +19,3 @@ spack # slow, success, but mypy-primer hangs processing the output
spark # too many iterations
steam.py # hangs (single threaded)
streamlit # too many iterations (uses packaging)
xarray # too many iterations

View File

@@ -10,8 +10,10 @@ aioredis
aiortc
alectryon
alerta
antidote
anyio
apprise
artigraph
async-utils
asynq
attrs
@@ -27,6 +29,7 @@ cloud-init
colour
com2ann
comtypes
core
cwltool
dacite
dd-trace-py
@@ -66,6 +69,8 @@ openlibrary
operator
optuna
paasta
pandas
pandas-stubs
paroxython
parso
pegen
@@ -114,6 +119,7 @@ urllib3
vision
websockets
werkzeug
xarray
xarray-dataclasses
yarl
zipp

View File

@@ -182,7 +182,7 @@ impl<'db> DunderAllNamesCollector<'db> {
///
/// This function panics if `expr` was not marked as a standalone expression during semantic indexing.
fn standalone_expression_type(&self, expr: &ast::Expr) -> Type<'db> {
infer_expression_types(self.db, self.index.expression(expr)).expression_type(expr)
infer_expression_types(self.db, self.index.expression(expr), false).expression_type(expr)
}
/// Evaluate the given expression and return its truthiness.

View File

@@ -328,10 +328,10 @@ fn singleton_to_type(db: &dyn Db, singleton: ruff_python_ast::Singleton) -> Type
fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>) -> Type<'db> {
match kind {
PatternPredicateKind::Singleton(singleton) => singleton_to_type(db, *singleton),
PatternPredicateKind::Value(value) => infer_expression_type(db, *value),
PatternPredicateKind::Value(value) => infer_expression_type(db, *value, false),
PatternPredicateKind::Class(class_expr, kind) => {
if kind.is_irrefutable() {
infer_expression_type(db, *class_expr)
infer_expression_type(db, *class_expr, false)
.to_instance(db)
.unwrap_or(Type::Never)
} else {
@@ -718,7 +718,7 @@ impl ReachabilityConstraints {
) -> Truthiness {
match predicate_kind {
PatternPredicateKind::Value(value) => {
let value_ty = infer_expression_type(db, *value);
let value_ty = infer_expression_type(db, *value, false);
if subject_ty.is_single_valued(db) {
Truthiness::from(subject_ty.is_equivalent_to(db, value_ty))
@@ -769,7 +769,7 @@ impl ReachabilityConstraints {
truthiness
}
PatternPredicateKind::Class(class_expr, kind) => {
let class_ty = infer_expression_type(db, *class_expr).to_instance(db);
let class_ty = infer_expression_type(db, *class_expr, false).to_instance(db);
class_ty.map_or(Truthiness::Ambiguous, |class_ty| {
if subject_ty.is_subtype_of(db, class_ty) {
@@ -797,7 +797,7 @@ impl ReachabilityConstraints {
}
fn analyze_single_pattern_predicate(db: &dyn Db, predicate: PatternPredicate) -> Truthiness {
let subject_ty = infer_expression_type(db, predicate.subject(db));
let subject_ty = infer_expression_type(db, predicate.subject(db), false);
let narrowed_subject_ty = IntersectionBuilder::new(db)
.add_positive(subject_ty)
@@ -837,7 +837,7 @@ impl ReachabilityConstraints {
// selection algorithm).
// Avoiding this on the happy-path is important because these constraints can be
// very large in number, since we add them on all statement level function calls.
let ty = infer_expression_type(db, callable);
let ty = infer_expression_type(db, callable, false);
// Short-circuit for well known types that are known not to return `Never` when called.
// Without the short-circuit, we've seen that threads keep blocking each other
@@ -875,7 +875,7 @@ impl ReachabilityConstraints {
} else if all_overloads_return_never {
Truthiness::AlwaysTrue
} else {
let call_expr_ty = infer_expression_type(db, call_expr);
let call_expr_ty = infer_expression_type(db, call_expr, false);
if call_expr_ty.is_equivalent_to(db, Type::Never) {
Truthiness::AlwaysTrue
} else {

View File

@@ -46,7 +46,7 @@ use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOO
pub use crate::types::display::DisplaySettings;
use crate::types::enums::{enum_metadata, is_single_member_enum};
use crate::types::function::{
DataclassTransformerParams, FunctionDecorators, FunctionSpans, FunctionType, KnownFunction,
DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction,
};
use crate::types::generics::{
GenericContext, PartialSpecialization, Specialization, bind_typevar, walk_generic_context,
@@ -191,7 +191,7 @@ pub(crate) struct IsDisjoint;
pub(crate) type IsEquivalentVisitor<'db, C> = PairVisitor<'db, IsEquivalent, C>;
pub(crate) struct IsEquivalent;
/// A [`CycleDetector`] for `find_legacy_typevars` methods.
/// A [`CycleDetector`] that is used in `find_legacy_typevars` methods.
pub(crate) type FindLegacyTypeVarsVisitor<'db> = CycleDetector<FindLegacyTypeVars, Type<'db>, ()>;
pub(crate) struct FindLegacyTypeVars;
@@ -489,13 +489,18 @@ fn walk_property_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
impl get_size2::GetSize for PropertyInstanceType<'_> {}
impl<'db> PropertyInstanceType<'db> {
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
fn apply_type_mapping_impl<'a>(
self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
let getter = self
.getter(db)
.map(|ty| ty.apply_type_mapping(db, type_mapping));
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor));
let setter = self
.setter(db)
.map(|ty| ty.apply_type_mapping(db, type_mapping));
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor));
Self::new(db, getter, setter)
}
@@ -1118,9 +1123,7 @@ impl<'db> Type<'db> {
#[must_use]
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
match self {
Type::Union(union) => {
visitor.visit(self, || Type::Union(union.normalized_impl(db, visitor)))
}
Type::Union(union) => visitor.visit(self, || union.normalized_impl(db, visitor)),
Type::Intersection(intersection) => visitor.visit(self, || {
Type::Intersection(intersection.normalized_impl(db, visitor))
}),
@@ -1887,14 +1890,14 @@ impl<'db> Type<'db> {
}
(Type::TypeAlias(self_alias), _) => {
let self_alias_ty = self_alias.value_type(db);
let self_alias_ty = self_alias.value_type(db).normalized(db);
visitor.visit((self_alias_ty, other), || {
self_alias_ty.is_equivalent_to_impl(db, other, visitor)
})
}
(_, Type::TypeAlias(other_alias)) => {
let other_alias_ty = other_alias.value_type(db);
let other_alias_ty = other_alias.value_type(db).normalized(db);
visitor.visit((self, other_alias_ty), || {
self.is_equivalent_to_impl(db, other_alias_ty, visitor)
})
@@ -1988,11 +1991,6 @@ impl<'db> Type<'db> {
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => C::unsatisfiable(db),
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
// TODO: Implement disjointness for TypedDict
C::unsatisfiable(db)
}
(Type::TypeAlias(alias), _) => {
let self_alias_ty = alias.value_type(db);
visitor.visit((self_alias_ty, other), || {
@@ -2007,6 +2005,11 @@ impl<'db> Type<'db> {
})
}
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
// TODO: Implement disjointness for TypedDict
C::unsatisfiable(db)
}
// A typevar is never disjoint from itself, since all occurrences of the typevar must
// be specialized to the same type. (This is an important difference between typevars
// and `Any`!) Different typevars might be disjoint, depending on their bounds and
@@ -3334,7 +3337,12 @@ impl<'db> Type<'db> {
name: Name,
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
tracing::trace!("member_lookup_with_policy: {}.{}", self.display(db), name);
let _span = tracing::trace_span!(
"member_lookup_with_policy",
ty = self.display(db).to_string(),
?name
)
.entered();
if name == "__class__" {
return Place::bound(self.dunder_class(db)).into();
}
@@ -5926,6 +5934,8 @@ impl<'db> Type<'db> {
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
Type::ProtocolInstance(protocol) => protocol.to_meta_type(db),
// `TypedDict` instances are instances of `dict` at runtime, but its important that we
// understand a more specific meta type in order to correctly handle `__getitem__`.
Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class()),
Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db),
}
@@ -6077,18 +6087,18 @@ impl<'db> Type<'db> {
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => {
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(
property.apply_type_mapping(db, type_mapping),
property.apply_type_mapping_impl(db, type_mapping, visitor),
))
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => {
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(
property.apply_type_mapping(db, type_mapping),
property.apply_type_mapping_impl(db, type_mapping, visitor),
))
}
Type::Callable(callable) => {
Type::Callable(callable.apply_type_mapping(db, type_mapping))
Type::Callable(callable.apply_type_mapping_impl(db, type_mapping, visitor))
}
Type::GenericAlias(generic) => {
@@ -6104,7 +6114,7 @@ impl<'db> Type<'db> {
),
Type::PropertyInstance(property) => {
Type::PropertyInstance(property.apply_type_mapping(db, type_mapping))
Type::PropertyInstance(property.apply_type_mapping_impl(db, type_mapping, visitor))
}
Type::Union(union) => union.map(db, |element| {
@@ -7528,6 +7538,28 @@ pub struct BoundTypeVarInstance<'db> {
impl get_size2::GetSize for BoundTypeVarInstance<'_> {}
impl<'db> BoundTypeVarInstance<'db> {
/// Create a new PEP 695 type variable that can be used in signatures
/// of synthetic generic functions.
pub(crate) fn synthetic(
db: &'db dyn Db,
name: &'static str,
variance: TypeVarVariance,
) -> Self {
Self::new(
db,
TypeVarInstance::new(
db,
Name::new_static(name),
None, // definition
None, // _bound_or_constraints
Some(variance),
None, // _default
TypeVarKind::Pep695,
),
BindingContext::Synthetic,
)
}
pub(crate) fn variance_with_polarity(
self,
db: &'db dyn Db,
@@ -7675,7 +7707,17 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
TypeVarBoundOrConstraints::UpperBound(bound.normalized_impl(db, visitor))
}
TypeVarBoundOrConstraints::Constraints(constraints) => {
TypeVarBoundOrConstraints::Constraints(constraints.normalized_impl(db, visitor))
// Constraints are a non-normalized union by design (it's not really a union at
// all, we are just using a union to store the types). Normalize the types but not
// the containing union.
TypeVarBoundOrConstraints::Constraints(UnionType::new(
db,
constraints
.elements(db)
.iter()
.map(|ty| ty.normalized_impl(db, visitor))
.collect::<Box<_>>(),
))
}
}
}
@@ -8782,10 +8824,7 @@ impl<'db> BoundMethodType<'db> {
/// a `@classmethod`, then it should be an instance of that bound-instance type.
pub(crate) fn typing_self_type(self, db: &'db dyn Db) -> Type<'db> {
let mut self_instance = self.self_instance(db);
if self
.function(db)
.has_known_decorator(db, FunctionDecorators::CLASSMETHOD)
{
if self.function(db).is_classmethod(db) {
self_instance = self_instance.to_instance(db).unwrap_or_else(Type::unknown);
}
self_instance
@@ -8953,10 +8992,16 @@ impl<'db> CallableType<'db> {
)
}
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
fn apply_type_mapping_impl<'a>(
self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
CallableType::new(
db,
self.signatures(db).apply_type_mapping(db, type_mapping),
self.signatures(db)
.apply_type_mapping_impl(db, type_mapping, visitor),
self.is_function_like(db),
)
}
@@ -9632,18 +9677,25 @@ impl<'db> UnionType<'db> {
///
/// See [`Type::normalized`] for more details.
#[must_use]
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
pub(crate) fn normalized(self, db: &'db dyn Db) -> Type<'db> {
self.normalized_impl(db, &NormalizedVisitor::default())
}
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
let mut new_elements: Vec<Type<'db>> = self
.elements(db)
pub(crate) fn normalized_impl(
self,
db: &'db dyn Db,
visitor: &NormalizedVisitor<'db>,
) -> Type<'db> {
self.elements(db)
.iter()
.map(|element| element.normalized_impl(db, visitor))
.collect();
new_elements.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r));
UnionType::new(db, new_elements.into_boxed_slice())
.map(|ty| ty.normalized_impl(db, visitor))
.fold(
UnionBuilder::new(db)
.order_elements(true)
.unpack_aliases(true),
UnionBuilder::add,
)
.build()
}
pub(crate) fn is_equivalent_to_impl<C: Constraints<'db>>(
@@ -9665,7 +9717,7 @@ impl<'db> UnionType<'db> {
let sorted_self = self.normalized(db);
if sorted_self == other {
if sorted_self == Type::Union(other) {
return C::always_satisfiable(db);
}
@@ -10376,11 +10428,7 @@ static_assertions::assert_eq_size!(Type, [u8; 16]);
pub(crate) mod tests {
use super::*;
use crate::db::tests::{TestDbBuilder, setup_db};
use crate::place::{global_symbol, typing_extensions_symbol, typing_symbol};
use ruff_db::files::system_path_to_file;
use ruff_db::parsed::parsed_module;
use ruff_db::system::DbWithWritableSystem as _;
use ruff_db::testing::assert_function_query_was_not_run;
use crate::place::{typing_extensions_symbol, typing_symbol};
use ruff_python_ast::PythonVersion;
use test_case::test_case;
@@ -10421,62 +10469,62 @@ pub(crate) mod tests {
/// Inferring the result of a call-expression shouldn't need to re-run after
/// a trivial change to the function's file (e.g. by adding a docstring to the function).
#[test]
fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> {
let mut db = setup_db();
// #[test]
// fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> {
// let mut db = setup_db();
db.write_dedented(
"src/foo.py",
r#"
def foo() -> int:
return 5
"#,
)?;
db.write_dedented(
"src/bar.py",
r#"
from foo import foo
// db.write_dedented(
// "src/foo.py",
// r#"
// def foo() -> int:
// return 5
// "#,
// )?;
// db.write_dedented(
// "src/bar.py",
// r#"
// from foo import foo
a = foo()
"#,
)?;
// a = foo()
// "#,
// )?;
let bar = system_path_to_file(&db, "src/bar.py")?;
let a = global_symbol(&db, bar, "a").place;
// let bar = system_path_to_file(&db, "src/bar.py")?;
// let a = global_symbol(&db, bar, "a").place;
assert_eq!(
a.expect_type(),
UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)])
);
// assert_eq!(
// a.expect_type(),
// UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)])
// );
// Add a docstring to foo to trigger a re-run.
// The bar-call site of foo should not be re-run because of that
db.write_dedented(
"src/foo.py",
r#"
def foo() -> int:
"Computes a value"
return 5
"#,
)?;
db.clear_salsa_events();
// // Add a docstring to foo to trigger a re-run.
// // The bar-call site of foo should not be re-run because of that
// db.write_dedented(
// "src/foo.py",
// r#"
// def foo() -> int:
// "Computes a value"
// return 5
// "#,
// )?;
// db.clear_salsa_events();
let a = global_symbol(&db, bar, "a").place;
// let a = global_symbol(&db, bar, "a").place;
assert_eq!(
a.expect_type(),
UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)])
);
let events = db.take_salsa_events();
// assert_eq!(
// a.expect_type(),
// UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)])
// );
// let events = db.take_salsa_events();
let module = parsed_module(&db, bar).load(&db);
let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value;
let foo_call = semantic_index(&db, bar).expression(call);
// let module = parsed_module(&db, bar).load(&db);
// let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value;
// let foo_call = semantic_index(&db, bar).expression(call);
assert_function_query_was_not_run(&db, infer_expression_types, foo_call, &events);
// assert_function_query_was_not_run(&db, infer_expression_types, foo_call, &events, false);
Ok(())
}
// Ok(())
// }
/// All other tests also make sure that `Type::Todo` works as expected. This particular
/// test makes sure that we handle `Todo` types correctly, even if they originate from

View File

@@ -38,6 +38,7 @@
//! unnecessary `is_subtype_of` checks.
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, StringLiteralType, Type,
TypeVarBoundOrConstraints, UnionType,
@@ -211,6 +212,7 @@ pub(crate) struct UnionBuilder<'db> {
elements: Vec<UnionElement<'db>>,
db: &'db dyn Db,
unpack_aliases: bool,
order_elements: bool,
}
impl<'db> UnionBuilder<'db> {
@@ -219,6 +221,7 @@ impl<'db> UnionBuilder<'db> {
db,
elements: vec![],
unpack_aliases: true,
order_elements: false,
}
}
@@ -227,6 +230,11 @@ impl<'db> UnionBuilder<'db> {
self
}
pub(crate) fn order_elements(mut self, val: bool) -> Self {
self.order_elements = val;
self
}
pub(crate) fn is_empty(&self) -> bool {
self.elements.is_empty()
}
@@ -545,6 +553,9 @@ impl<'db> UnionBuilder<'db> {
UnionElement::Type(ty) => types.push(ty),
}
}
if self.order_elements {
types.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(self.db, l, r));
}
match types.len() {
0 => None,
1 => Some(types[0]),

View File

@@ -272,7 +272,7 @@ impl<'db> Bindings<'db> {
for (overload_index, overload) in binding.matching_overloads_mut() {
match binding_type {
Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => {
if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
if function.is_classmethod(db) {
match overload.parameter_types() {
[_, Some(owner)] => {
overload.set_return_type(Type::BoundMethod(
@@ -308,7 +308,7 @@ impl<'db> Bindings<'db> {
if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] =
overload.parameter_types()
{
if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) {
if function.is_classmethod(db) {
match overload.parameter_types() {
[_, _, Some(owner)] => {
overload.set_return_type(Type::BoundMethod(

View File

@@ -34,7 +34,7 @@ use crate::types::{
IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind,
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams,
VarianceInferable, declaration_type, infer_definition_types, todo_type,
UnionBuilder, VarianceInferable, declaration_type, infer_definition_types,
};
use crate::{
Db, FxIndexMap, FxOrderSet, Program,
@@ -51,7 +51,7 @@ use crate::{
semantic_index, use_def_map,
},
types::{
CallArguments, CallError, CallErrorKind, MetaclassCandidate, UnionBuilder, UnionType,
CallArguments, CallError, CallErrorKind, MetaclassCandidate, UnionType,
definition_expression_type,
},
};
@@ -1258,7 +1258,7 @@ pub(super) enum MethodDecorator {
impl MethodDecorator {
fn try_from_fn_type(db: &dyn Db, fn_type: FunctionType) -> Result<Self, ()> {
match (
fn_type.has_known_decorator(db, FunctionDecorators::CLASSMETHOD),
fn_type.is_classmethod(db),
fn_type.has_known_decorator(db, FunctionDecorators::STATICMETHOD),
) {
(true, true) => Err(()), // A method can't be static and class method at the same time.
@@ -2331,49 +2331,179 @@ impl<'db> ClassLiteral<'db> {
)))
}
(CodeGeneratorKind::TypedDict, "get") => {
// TODO: synthesize a set of overloads with precise types
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
Parameter::positional_only(Some(Name::new_static("default")))
.with_default_type(Type::unknown()),
]),
Some(todo_type!("Support for `TypedDict`")),
);
let overloads = self
.fields(db, specialization, field_policy)
.into_iter()
.flat_map(|(name, field)| {
let key_type =
Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
Some(CallableType::function_like(db, signature))
// For a required key, `.get()` always returns the value type. For a non-required key,
// `.get()` returns the union of the value type and the type of the default argument
// (which defaults to `None`).
// TODO: For now, we use two overloads here. They can be merged into a single function
// once the generics solver takes default arguments into account.
let get_sig = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key")))
.with_annotated_type(key_type),
]),
Some(if field.is_required() {
field.declared_ty
} else {
UnionType::from_elements(db, [field.declared_ty, Type::none(db)])
}),
);
let t_default =
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
let get_with_default_sig = Signature::new_generic(
Some(GenericContext::from_typevar_instances(db, [t_default])),
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key")))
.with_annotated_type(key_type),
Parameter::positional_only(Some(Name::new_static("default")))
.with_annotated_type(Type::TypeVar(t_default)),
]),
Some(if field.is_required() {
field.declared_ty
} else {
UnionType::from_elements(
db,
[field.declared_ty, Type::TypeVar(t_default)],
)
}),
);
[get_sig, get_with_default_sig]
})
// Fallback overloads for unknown keys
.chain(std::iter::once({
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key")))
.with_annotated_type(KnownClass::Str.to_instance(db)),
]),
Some(UnionType::from_elements(
db,
[Type::unknown(), Type::none(db)],
)),
)
}))
.chain(std::iter::once({
let t_default =
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
Signature::new_generic(
Some(GenericContext::from_typevar_instances(db, [t_default])),
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key")))
.with_annotated_type(KnownClass::Str.to_instance(db)),
Parameter::positional_only(Some(Name::new_static("default")))
.with_annotated_type(Type::TypeVar(t_default)),
]),
Some(UnionType::from_elements(
db,
[Type::unknown(), Type::TypeVar(t_default)],
)),
)
}));
Some(Type::Callable(CallableType::new(
db,
CallableSignature::from_overloads(overloads),
true,
)))
}
(CodeGeneratorKind::TypedDict, "pop") => {
// TODO: synthesize a set of overloads with precise types.
// Required keys should be forbidden to be popped.
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
Parameter::positional_only(Some(Name::new_static("default")))
.with_default_type(Type::unknown()),
]),
Some(todo_type!("Support for `TypedDict`")),
);
let fields = self.fields(db, specialization, field_policy);
let overloads = fields
.iter()
.filter(|(_, field)| {
// Only synthesize `pop` for fields that are not required.
!field.is_required()
})
.flat_map(|(name, field)| {
let key_type =
Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
Some(CallableType::function_like(db, signature))
// TODO: Similar to above: consider merging these two overloads into one
// `.pop()` without default
let pop_sig = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key")))
.with_annotated_type(key_type),
]),
Some(field.declared_ty),
);
// `.pop()` with a default value
let t_default =
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
let pop_with_default_sig = Signature::new_generic(
Some(GenericContext::from_typevar_instances(db, [t_default])),
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key")))
.with_annotated_type(key_type),
Parameter::positional_only(Some(Name::new_static("default")))
.with_annotated_type(Type::TypeVar(t_default)),
]),
Some(UnionType::from_elements(
db,
[field.declared_ty, Type::TypeVar(t_default)],
)),
);
[pop_sig, pop_with_default_sig]
});
Some(Type::Callable(CallableType::new(
db,
CallableSignature::from_overloads(overloads),
true,
)))
}
(CodeGeneratorKind::TypedDict, "setdefault") => {
// TODO: synthesize a set of overloads with precise types
let signature = Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key"))),
Parameter::positional_only(Some(Name::new_static("default"))),
]),
Some(todo_type!("Support for `TypedDict`")),
);
let fields = self.fields(db, specialization, field_policy);
let overloads = fields.iter().map(|(name, field)| {
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
Some(CallableType::function_like(db, signature))
// `setdefault` always returns the field type
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(instance_ty),
Parameter::positional_only(Some(Name::new_static("key")))
.with_annotated_type(key_type),
Parameter::positional_only(Some(Name::new_static("default")))
.with_annotated_type(field.declared_ty),
]),
Some(field.declared_ty),
)
});
Some(Type::Callable(CallableType::new(
db,
CallableSignature::from_overloads(overloads),
true,
)))
}
(CodeGeneratorKind::TypedDict, "update") => {
// TODO: synthesize a set of overloads with precise types
@@ -2752,7 +2882,7 @@ impl<'db> ClassLiteral<'db> {
// `self.SOME_CONSTANT: Final = 1`, infer the type from the value
// on the right-hand side.
let inferred_ty = infer_expression_type(db, index.expression(value));
let inferred_ty = infer_expression_type(db, index.expression(value), true);
return Place::bound(inferred_ty).with_qualifiers(all_qualifiers);
}
@@ -2838,6 +2968,7 @@ impl<'db> ClassLiteral<'db> {
let inferred_ty = infer_expression_type(
db,
index.expression(assign.value(&module)),
true,
);
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
@@ -2865,6 +2996,7 @@ impl<'db> ClassLiteral<'db> {
let iterable_ty = infer_expression_type(
db,
index.expression(for_stmt.iterable(&module)),
true,
);
// TODO: Potential diagnostics resulting from the iterable are currently not reported.
let inferred_ty =
@@ -2895,6 +3027,7 @@ impl<'db> ClassLiteral<'db> {
let context_ty = infer_expression_type(
db,
index.expression(with_item.context_expr(&module)),
true,
);
let inferred_ty = if with_item.is_async() {
context_ty.aenter(db)
@@ -2928,6 +3061,7 @@ impl<'db> ClassLiteral<'db> {
let iterable_ty = infer_expression_type(
db,
index.expression(comprehension.iterable(&module)),
true,
);
// TODO: Potential diagnostics resulting from the iterable are currently not reported.
let inferred_ty =

View File

@@ -2952,6 +2952,21 @@ pub(crate) fn report_missing_typed_dict_key<'db>(
}
}
pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>(
context: &InferContext<'db, '_>,
key_node: AnyNodeRef,
typed_dict_ty: Type<'db>,
field_name: &str,
) {
let db = context.db();
if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, key_node) {
let typed_dict_name = typed_dict_ty.display(db);
builder.into_diagnostic(format_args!(
"Cannot pop required field '{field_name}' from TypedDict `{typed_dict_name}`",
));
}
}
/// This function receives an unresolved `from foo import bar` import,
/// where `foo` can be resolved to a module but that module does not
/// have a `bar` member or submodule.

View File

@@ -569,7 +569,7 @@ impl<'db> FunctionLiteral<'db> {
if overloads.is_empty() {
return CallableSignature::single(type_mappings.iter().fold(
implementation.signature(db, inherited_generic_context),
|ty, mapping| ty.apply_type_mapping(db, mapping),
|sig, mapping| sig.apply_type_mapping(db, mapping),
));
}
}
@@ -577,7 +577,7 @@ impl<'db> FunctionLiteral<'db> {
CallableSignature::from_overloads(overloads.iter().map(|overload| {
type_mappings.iter().fold(
overload.signature(db, inherited_generic_context),
|ty, mapping| ty.apply_type_mapping(db, mapping),
|sig, mapping| sig.apply_type_mapping(db, mapping),
)
}))
}
@@ -602,7 +602,7 @@ impl<'db> FunctionLiteral<'db> {
type_mappings.iter().fold(
self.last_definition(db)
.signature(db, inherited_generic_context),
|ty, mapping| ty.apply_type_mapping(db, mapping),
|sig, mapping| sig.apply_type_mapping(db, mapping),
)
}
@@ -721,6 +721,16 @@ impl<'db> FunctionType<'db> {
self.literal(db).has_known_decorator(db, decorator)
}
/// Returns true if this method is decorated with `@classmethod`, or if it is implicitly a
/// classmethod.
pub(crate) fn is_classmethod(self, db: &'db dyn Db) -> bool {
self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD)
|| matches!(
self.name(db).as_str(),
"__init_subclass__" | "__class_getitem__"
)
}
/// If the implementation of this function is deprecated, returns the `@warnings.deprecated`.
///
/// Checking if an overload is deprecated requires deeper call analysis.

View File

@@ -119,13 +119,19 @@ impl<'db> GenericContext<'db> {
binding_context: Definition<'db>,
type_params_node: &ast::TypeParams,
) -> Self {
let variables: FxOrderSet<_> = type_params_node
.iter()
.filter_map(|type_param| {
Self::variable_from_type_param(db, index, binding_context, type_param)
})
.collect();
Self::new(db, variables)
let variables = type_params_node.iter().filter_map(|type_param| {
Self::variable_from_type_param(db, index, binding_context, type_param)
});
Self::from_typevar_instances(db, variables)
}
/// Creates a generic context from a list of `BoundTypeVarInstance`s.
pub(crate) fn from_typevar_instances(
db: &'db dyn Db,
type_params: impl IntoIterator<Item = BoundTypeVarInstance<'db>>,
) -> Self {
Self::new(db, type_params.into_iter().collect::<FxOrderSet<_>>())
}
fn variable_from_type_param(
@@ -365,12 +371,12 @@ impl<'db> GenericContext<'db> {
}
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
let variables: FxOrderSet<_> = self
let variables = self
.variables(db)
.iter()
.map(|bound_typevar| bound_typevar.normalized_impl(db, visitor))
.collect();
Self::new(db, variables)
.map(|bound_typevar| bound_typevar.normalized_impl(db, visitor));
Self::from_typevar_instances(db, variables)
}
fn heap_size((variables,): &(FxOrderSet<BoundTypeVarInstance<'db>>,)) -> usize {

View File

@@ -102,7 +102,8 @@ use crate::types::diagnostic::{
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL,
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR,
report_bad_dunder_set_call, report_implicit_return_type, report_instance_layout_conflict,
report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
report_implicit_return_type, report_instance_layout_conflict,
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
@@ -255,6 +256,7 @@ fn deferred_cycle_initial<'db>(
pub(crate) fn infer_expression_types<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
_break_cycle: bool,
) -> ExpressionInference<'db> {
let file = expression.file(db);
let module = parsed_module(db, file).load(db);
@@ -277,6 +279,7 @@ fn expression_cycle_recover<'db>(
_value: &ExpressionInference<'db>,
_count: u32,
_expression: Expression<'db>,
_break_cycle: bool,
) -> salsa::CycleRecoveryAction<ExpressionInference<'db>> {
salsa::CycleRecoveryAction::Iterate
}
@@ -284,6 +287,7 @@ fn expression_cycle_recover<'db>(
fn expression_cycle_initial<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
_break_cycle: bool,
) -> ExpressionInference<'db> {
ExpressionInference::cycle_fallback(expression.scope(db))
}
@@ -297,8 +301,9 @@ pub(super) fn infer_same_file_expression_type<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
parsed: &ParsedModuleRef,
break_cycle: bool,
) -> Type<'db> {
let inference = infer_expression_types(db, expression);
let inference = infer_expression_types(db, expression, break_cycle);
inference.expression_type(expression.node_ref(db, parsed))
}
@@ -313,19 +318,33 @@ pub(super) fn infer_same_file_expression_type<'db>(
pub(crate) fn infer_expression_type<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
break_cycle: bool,
) -> Type<'db> {
let file = expression.file(db);
let module = parsed_module(db, file).load(db);
// It's okay to call the "same file" version here because we're inside a salsa query.
infer_same_file_expression_type(db, expression, &module)
infer_same_file_expression_type(db, expression, &module, break_cycle)
}
// #[salsa::tracked(cycle_fn=single_expression_cycle_recover, cycle_initial=single_expression_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
// pub(crate) fn infer_expression_type_query<'db>(
// db: &'db dyn Db,
// expression: Expression<'db>,
// ) -> Type<'db> {
// let file = expression.file(db);
// let module = parsed_module(db, file).load(db);
// // It's okay to call the "same file" version here because we're inside a salsa query.
// infer_same_file_expression_type(db, expression, &module)
// }
fn single_expression_cycle_recover<'db>(
_db: &'db dyn Db,
_value: &Type<'db>,
_count: u32,
_expression: Expression<'db>,
_break_cycle: bool,
) -> salsa::CycleRecoveryAction<Type<'db>> {
salsa::CycleRecoveryAction::Iterate
}
@@ -333,6 +352,7 @@ fn single_expression_cycle_recover<'db>(
fn single_expression_cycle_initial<'db>(
_db: &'db dyn Db,
_expression: Expression<'db>,
_break_cycle: bool,
) -> Type<'db> {
Type::Never
}
@@ -346,7 +366,7 @@ pub(crate) fn static_expression_truthiness<'db>(
db: &'db dyn Db,
expression: Expression<'db>,
) -> Truthiness {
let inference = infer_expression_types(db, expression);
let inference = infer_expression_types(db, expression, false);
if !inference.all_places_definitely_bound() {
return Truthiness::Ambiguous;
@@ -998,9 +1018,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.index.has_future_annotations() || self.in_stub()
}
/// Are we currently inferring deferred types?
/// Are we currently in a context where name resolution should be deferred
/// (`__future__.annotations`, stub file, or stringified annotation)?
fn is_deferred(&self) -> bool {
matches!(self.region, InferenceRegion::Deferred(_)) || self.deferred_state.is_deferred()
self.deferred_state.is_deferred()
}
/// Return the node key of the given AST node, or the key of the outermost enclosing string
@@ -2674,7 +2695,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_definition(&mut self, node: impl Into<DefinitionNodeKey> + std::fmt::Debug + Copy) {
let definition = self.index.expect_single_definition(node);
let result = infer_definition_types(self.db(), definition);
self.extend_definition(result);
self.extend_definition(&result);
}
fn infer_function_definition_statement(&mut self, function: &ast::StmtFunctionDef) {
@@ -3172,10 +3193,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_expression(&keyword.value);
}
// Inference of bases deferred in stubs
// TODO: Only defer the references that are actually string literals, instead of
// deferring the entire class definition if a string literal occurs anywhere in the
// base class list.
// Inference of bases deferred in stubs, or if any are string literals.
if self.in_stub() || class_node.bases().iter().any(contains_string_literal) {
self.deferred.insert(definition);
} else {
@@ -3206,7 +3224,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
fn infer_class_deferred(&mut self, definition: Definition<'db>, class: &ast::StmtClassDef) {
let previous_typevar_binding_context = self.typevar_binding_context.replace(definition);
for base in class.bases() {
self.infer_expression(base);
if self.in_stub() {
self.infer_expression_with_state(base, DeferredExpressionState::Deferred);
} else {
self.infer_expression(base);
}
}
self.typevar_binding_context = previous_typevar_binding_context;
}
@@ -3560,6 +3582,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
bound,
default,
} = node;
let previous_deferred_state =
std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred);
match bound.as_deref() {
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
// We don't use UnionType::from_elements or UnionBuilder here, because we don't
@@ -3581,6 +3605,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
None => {}
}
self.infer_optional_type_expression(default.as_deref());
self.deferred_state = previous_deferred_state;
}
fn infer_paramspec_definition(
@@ -5136,7 +5161,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.check_deprecated(alias, ty.inner);
}
}
self.extend_definition(inferred);
self.extend_definition(&inferred);
}
}
}
@@ -5599,6 +5624,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_expression_impl(expression)
}
fn infer_expression_with_state(
&mut self,
expression: &ast::Expr,
state: DeferredExpressionState,
) -> Type<'db> {
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state);
let ty = self.infer_expression(expression);
self.deferred_state = previous_deferred_state;
ty
}
fn infer_maybe_standalone_expression(&mut self, expression: &ast::Expr) -> Type<'db> {
if let Some(standalone_expression) = self.index.try_expression(expression) {
self.infer_standalone_expression_impl(expression, standalone_expression)
@@ -5618,7 +5654,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
expression: &ast::Expr,
standalone_expression: Expression<'db>,
) -> Type<'db> {
let types = infer_expression_types(self.db(), standalone_expression);
let types = infer_expression_types(self.db(), standalone_expression, false);
self.extend_expression(types);
// Instead of calling `self.expression_type(expr)` after extending here, we get
@@ -6052,6 +6088,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
builder.db(),
builder.index.expression(iter_expr),
builder.module(),
false,
)
} else {
builder.infer_standalone_expression(iter_expr)
@@ -6074,7 +6111,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut infer_iterable_type = || {
let expression = self.index.expression(iterable);
let result = infer_expression_types(self.db(), expression);
let result = infer_expression_types(self.db(), expression, false);
// Two things are different if it's the first comprehension:
// (1) We must lookup the `ScopedExpressionId` of the iterable expression in the outer scope,
@@ -6124,7 +6161,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if named.target.is_name_expr() {
let definition = self.index.expect_single_definition(named);
let result = infer_definition_types(self.db(), definition);
self.extend_definition(result);
self.extend_definition(&result);
result.binding_type(definition)
} else {
// For syntactically invalid targets, we still need to run type inference:
@@ -6270,6 +6307,58 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let callable_type = self.infer_maybe_standalone_expression(func);
// Special handling for `TypedDict` method calls
if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
let value_type = self.expression_type(value);
if let Type::TypedDict(typed_dict_ty) = value_type {
if matches!(attr.id.as_str(), "pop" | "setdefault") && !arguments.args.is_empty() {
// Validate the key argument for `TypedDict` methods
if let Some(first_arg) = arguments.args.first() {
if let ast::Expr::StringLiteral(ast::ExprStringLiteral {
value: key_literal,
..
}) = first_arg
{
let key = key_literal.to_str();
let items = typed_dict_ty.items(self.db());
// Check if key exists
if let Some((_, field)) = items
.iter()
.find(|(field_name, _)| field_name.as_str() == key)
{
// Key exists - check if it's a `pop()` on a required field
if attr.id.as_str() == "pop" && field.is_required() {
report_cannot_pop_required_field_on_typed_dict(
&self.context,
first_arg.into(),
Type::TypedDict(typed_dict_ty),
key,
);
return Type::unknown();
}
} else {
// Key not found, report error with suggestion and return early
let key_ty = Type::StringLiteral(
crate::types::StringLiteralType::new(self.db(), key),
);
report_invalid_key_on_typed_dict(
&self.context,
first_arg.into(),
first_arg.into(),
Type::TypedDict(typed_dict_ty),
key_ty,
&items,
);
// Return `Unknown` to prevent the overload system from generating its own error
return Type::unknown();
}
}
}
}
}
}
if let Type::FunctionLiteral(function) = callable_type {
// Make sure that the `function.definition` is only called when the function is defined
// in the same file as the one we're currently inferring the types for. This is because
@@ -7170,13 +7259,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
/// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context.
fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
let ast::ExprAttribute {
value,
attr,
range: _,
node_index: _,
ctx: _,
} = attribute;
let ast::ExprAttribute { value, attr, .. } = attribute;
let value_type = self.infer_maybe_standalone_expression(value);
let db = self.db();
@@ -9140,7 +9223,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
match ty.try_call(db, &CallArguments::positional([value_ty, slice_ty])) {
match ty.try_call(db, &CallArguments::positional([slice_ty])) {
Ok(bindings) => return bindings.return_type(db),
Err(CallError(_, bindings)) => {
if let Some(builder) =
@@ -11604,7 +11687,7 @@ mod tests {
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, system_path_to_file};
use ruff_db::system::DbWithWritableSystem as _;
use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run};
use ruff_db::testing::assert_function_query_was_not_run;
use super::*;
@@ -11986,267 +12069,267 @@ mod tests {
Ok(())
}
#[test]
fn dependency_implicit_instance_attribute() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main).load(db);
// Get the second statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
// #[test]
// fn dependency_implicit_instance_attribute() -> anyhow::Result<()> {
// fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
// let file_main = system_path_to_file(db, "/src/main.py").unwrap();
// let ast = parsed_module(db, file_main).load(db);
// // Get the second statement in `main.py` (x = …) and extract the expression
// // node on the right-hand side:
// let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
let index = semantic_index(db, file_main);
index.expression(x_rhs_node.as_ref())
}
// let index = semantic_index(db, file_main);
// index.expression(x_rhs_node.as_ref())
// }
let mut db = setup_db();
// let mut db = setup_db();
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
self.attr: int | None = None
"#,
)?;
db.write_dedented(
"/src/main.py",
r#"
from mod import C
x = C().attr
"#,
)?;
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// def f(self):
// self.attr: int | None = None
// "#,
// )?;
// db.write_dedented(
// "/src/main.py",
// r#"
// from mod import C
// x = C().attr
// "#,
// )?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
self.attr: str | None = None
"#,
)?;
// // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// def f(self):
// self.attr: str | None = None
// "#,
// )?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// let events = {
// db.clear_salsa_events();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
// db.take_salsa_events()
// };
// assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// Add a comment; this should not trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def f(self):
# a comment!
self.attr: str | None = None
"#,
)?;
// // Add a comment; this should not trigger the type of `x` to be re-inferred
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// def f(self):
// # a comment!
// self.attr: str | None = None
// "#,
// )?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
// let events = {
// db.clear_salsa_events();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
// db.take_salsa_events()
// };
assert_function_query_was_not_run(
&db,
infer_expression_types,
x_rhs_expression(&db),
&events,
);
// assert_function_query_was_not_run(
// &db,
// infer_expression_types,
// x_rhs_expression(&db),
// &events,
// );
Ok(())
}
// Ok(())
// }
/// This test verifies that changing a class's declaration in a non-meaningful way (e.g. by adding a comment)
/// doesn't trigger type inference for expressions that depend on the class's members.
#[test]
fn dependency_own_instance_member() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main).load(db);
// Get the second statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
// /// This test verifies that changing a class's declaration in a non-meaningful way (e.g. by adding a comment)
// /// doesn't trigger type inference for expressions that depend on the class's members.
// #[test]
// fn dependency_own_instance_member() -> anyhow::Result<()> {
// fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
// let file_main = system_path_to_file(db, "/src/main.py").unwrap();
// let ast = parsed_module(db, file_main).load(db);
// // Get the second statement in `main.py` (x = …) and extract the expression
// // node on the right-hand side:
// let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value;
let index = semantic_index(db, file_main);
index.expression(x_rhs_node.as_ref())
}
// let index = semantic_index(db, file_main);
// index.expression(x_rhs_node.as_ref())
// }
let mut db = setup_db();
// let mut db = setup_db();
db.write_dedented(
"/src/mod.py",
r#"
class C:
if random.choice([True, False]):
attr: int = 42
else:
attr: None = None
"#,
)?;
db.write_dedented(
"/src/main.py",
r#"
from mod import C
x = C().attr
"#,
)?;
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// if random.choice([True, False]):
// attr: int = 42
// else:
// attr: None = None
// "#,
// )?;
// db.write_dedented(
// "/src/main.py",
// r#"
// from mod import C
// x = C().attr
// "#,
// )?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int | None");
// Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
if random.choice([True, False]):
attr: str = "42"
else:
attr: None = None
"#,
)?;
// // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// if random.choice([True, False]):
// attr: str = "42"
// else:
// attr: None = None
// "#,
// )?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// let events = {
// db.clear_salsa_events();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
// db.take_salsa_events()
// };
// assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// Add a comment; this should not trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
# comment
if random.choice([True, False]):
attr: str = "42"
else:
attr: None = None
"#,
)?;
// // Add a comment; this should not trigger the type of `x` to be re-inferred
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// # comment
// if random.choice([True, False]):
// attr: str = "42"
// else:
// attr: None = None
// "#,
// )?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
// let events = {
// db.clear_salsa_events();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
// db.take_salsa_events()
// };
assert_function_query_was_not_run(
&db,
infer_expression_types,
x_rhs_expression(&db),
&events,
);
// assert_function_query_was_not_run(
// &db,
// infer_expression_types,
// x_rhs_expression(&db),
// &events,
// );
Ok(())
}
// Ok(())
// }
#[test]
fn dependency_implicit_class_member() -> anyhow::Result<()> {
fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
let file_main = system_path_to_file(db, "/src/main.py").unwrap();
let ast = parsed_module(db, file_main).load(db);
// Get the third statement in `main.py` (x = …) and extract the expression
// node on the right-hand side:
let x_rhs_node = &ast.syntax().body[2].as_assign_stmt().unwrap().value;
// #[test]
// fn dependency_implicit_class_member() -> anyhow::Result<()> {
// fn x_rhs_expression(db: &TestDb) -> Expression<'_> {
// let file_main = system_path_to_file(db, "/src/main.py").unwrap();
// let ast = parsed_module(db, file_main).load(db);
// // Get the third statement in `main.py` (x = …) and extract the expression
// // node on the right-hand side:
// let x_rhs_node = &ast.syntax().body[2].as_assign_stmt().unwrap().value;
let index = semantic_index(db, file_main);
index.expression(x_rhs_node.as_ref())
}
// let index = semantic_index(db, file_main);
// index.expression(x_rhs_node.as_ref())
// }
let mut db = setup_db();
// let mut db = setup_db();
db.write_dedented(
"/src/mod.py",
r#"
class C:
def __init__(self):
self.instance_attr: str = "24"
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// def __init__(self):
// self.instance_attr: str = "24"
@classmethod
def method(cls):
cls.class_attr: int = 42
"#,
)?;
db.write_dedented(
"/src/main.py",
r#"
from mod import C
C.method()
x = C().class_attr
"#,
)?;
// @classmethod
// def method(cls):
// cls.class_attr: int = 42
// "#,
// )?;
// db.write_dedented(
// "/src/main.py",
// r#"
// from mod import C
// C.method()
// x = C().class_attr
// "#,
// )?;
let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int");
// let file_main = system_path_to_file(&db, "/src/main.py").unwrap();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | int");
// Change the type of `class_attr` to `str`; this should trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def __init__(self):
self.instance_attr: str = "24"
// // Change the type of `class_attr` to `str`; this should trigger the type of `x` to be re-inferred
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// def __init__(self):
// self.instance_attr: str = "24"
@classmethod
def method(cls):
cls.class_attr: str = "42"
"#,
)?;
// @classmethod
// def method(cls):
// cls.class_attr: str = "42"
// "#,
// )?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str");
db.take_salsa_events()
};
assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// let events = {
// db.clear_salsa_events();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str");
// db.take_salsa_events()
// };
// assert_function_query_was_run(&db, infer_expression_types, x_rhs_expression(&db), &events);
// Add a comment; this should not trigger the type of `x` to be re-inferred
db.write_dedented(
"/src/mod.py",
r#"
class C:
def __init__(self):
self.instance_attr: str = "24"
// // Add a comment; this should not trigger the type of `x` to be re-inferred
// db.write_dedented(
// "/src/mod.py",
// r#"
// class C:
// def __init__(self):
// self.instance_attr: str = "24"
@classmethod
def method(cls):
# comment
cls.class_attr: str = "42"
"#,
)?;
// @classmethod
// def method(cls):
// # comment
// cls.class_attr: str = "42"
// "#,
// )?;
let events = {
db.clear_salsa_events();
let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str");
db.take_salsa_events()
};
// let events = {
// db.clear_salsa_events();
// let attr_ty = global_symbol(&db, file_main, "x").place.expect_type();
// assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str");
// db.take_salsa_events()
// };
assert_function_query_was_not_run(
&db,
infer_expression_types,
x_rhs_expression(&db),
&events,
);
// assert_function_query_was_not_run(
// &db,
// infer_expression_types,
// x_rhs_expression(&db),
// &events,
// );
Ok(())
}
// Ok(())
// }
}

View File

@@ -709,7 +709,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
return None;
}
let inference = infer_expression_types(self.db, expression);
let inference = infer_expression_types(self.db, expression, false);
let comparator_tuples = std::iter::once(&**left)
.chain(comparators)
@@ -799,7 +799,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
expression: Expression<'db>,
is_positive: bool,
) -> Option<NarrowingConstraints<'db>> {
let inference = infer_expression_types(self.db, expression);
let inference = infer_expression_types(self.db, expression, false);
let callable_ty = inference.expression_type(&*expr_call.func);
@@ -921,7 +921,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
let subject = place_expr(subject.node_ref(self.db, self.module))?;
let place = self.expect_place(&subject);
let ty = infer_same_file_expression_type(self.db, cls, self.module).to_instance(self.db)?;
let ty = infer_same_file_expression_type(self.db, cls, self.module, false)
.to_instance(self.db)?;
Some(NarrowingConstraints::from_iter([(place, ty)]))
}
@@ -934,7 +935,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
let subject = place_expr(subject.node_ref(self.db, self.module))?;
let place = self.expect_place(&subject);
let ty = infer_same_file_expression_type(self.db, value, self.module);
let ty = infer_same_file_expression_type(self.db, value, self.module, false);
Some(NarrowingConstraints::from_iter([(place, ty)]))
}
@@ -963,7 +964,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
expression: Expression<'db>,
is_positive: bool,
) -> Option<NarrowingConstraints<'db>> {
let inference = infer_expression_types(self.db, expression);
let inference = infer_expression_types(self.db, expression, false);
let mut sub_constraints = expr_bool_op
.values
.iter()

View File

@@ -17,10 +17,10 @@ use crate::{
place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations},
semantic_index::{definition::Definition, use_def_map},
types::{
BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsDisjointVisitor, KnownFunction, MaterializationKind,
NormalizedVisitor, PropertyInstanceType, Signature, Type, TypeMapping, TypeQualifiers,
TypeRelation, VarianceInferable,
ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral,
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, KnownFunction,
MaterializationKind, NormalizedVisitor, PropertyInstanceType, Signature, Type, TypeMapping,
TypeQualifiers, TypeRelation, VarianceInferable,
constraints::{Constraints, IteratorConstraintsExtension},
signatures::{Parameter, Parameters},
},
@@ -282,7 +282,12 @@ impl<'db> ProtocolInterface<'db> {
.map(|(name, data)| {
(
name.clone(),
data.apply_type_mapping(db, type_mapping).normalized(db),
data.apply_type_mapping_impl(
db,
type_mapping,
&ApplyTypeMappingVisitor::default(),
)
.normalized(db),
)
})
.collect::<BTreeMap<_, _>>(),
@@ -354,9 +359,14 @@ impl<'db> ProtocolMemberData<'db> {
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
fn apply_type_mapping_impl<'a>(
&self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
Self {
kind: self.kind.apply_type_mapping(db, type_mapping),
kind: self.kind.apply_type_mapping_impl(db, type_mapping, visitor),
qualifiers: self.qualifiers,
}
}
@@ -444,16 +454,21 @@ impl<'db> ProtocolMemberKind<'db> {
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
fn apply_type_mapping_impl<'a>(
&self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
match self {
ProtocolMemberKind::Method(callable) => {
ProtocolMemberKind::Method(callable.apply_type_mapping(db, type_mapping))
}
ProtocolMemberKind::Property(property) => {
ProtocolMemberKind::Property(property.apply_type_mapping(db, type_mapping))
}
ProtocolMemberKind::Method(callable) => ProtocolMemberKind::Method(
callable.apply_type_mapping_impl(db, type_mapping, visitor),
),
ProtocolMemberKind::Property(property) => ProtocolMemberKind::Property(
property.apply_type_mapping_impl(db, type_mapping, visitor),
),
ProtocolMemberKind::Other(ty) => {
ProtocolMemberKind::Other(ty.apply_type_mapping(db, type_mapping))
ProtocolMemberKind::Other(ty.apply_type_mapping_impl(db, type_mapping, visitor))
}
}
}

View File

@@ -20,9 +20,9 @@ use crate::semantic_index::definition::Definition;
use crate::types::constraints::{ConstraintSet, Constraints, IteratorConstraintsExtension};
use crate::types::generics::{GenericContext, walk_generic_context};
use crate::types::{
BindingContext, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsEquivalentVisitor, KnownClass, MaterializationKind, NormalizedVisitor, TypeMapping,
TypeRelation, VarianceInferable, todo_type,
ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, NormalizedVisitor,
TypeMapping, TypeRelation, VarianceInferable, todo_type,
};
use crate::{Db, FxOrderSet};
use ruff_python_ast::{self as ast, name::Name};
@@ -82,15 +82,16 @@ impl<'db> CallableSignature<'db> {
)
}
pub(crate) fn apply_type_mapping<'a>(
pub(crate) fn apply_type_mapping_impl<'a>(
&self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
Self::from_overloads(
self.overloads
.iter()
.map(|signature| signature.apply_type_mapping(db, type_mapping)),
.map(|signature| signature.apply_type_mapping_impl(db, type_mapping, visitor)),
)
}
@@ -458,15 +459,26 @@ impl<'db> Signature<'db> {
&self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
) -> Self {
self.apply_type_mapping_impl(db, type_mapping, &ApplyTypeMappingVisitor::default())
}
pub(crate) fn apply_type_mapping_impl<'a>(
&self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
definition: self.definition,
parameters: self.parameters.apply_type_mapping(db, type_mapping),
parameters: self
.parameters
.apply_type_mapping_impl(db, type_mapping, visitor),
return_ty: self
.return_ty
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)),
}
}
@@ -504,7 +516,11 @@ impl<'db> Signature<'db> {
let mut parameters = Parameters::new(self.parameters().iter().skip(1).cloned());
let mut return_ty = self.return_ty;
if let Some(self_type) = self_type {
parameters = parameters.apply_type_mapping(db, &TypeMapping::BindSelf(self_type));
parameters = parameters.apply_type_mapping_impl(
db,
&TypeMapping::BindSelf(self_type),
&ApplyTypeMappingVisitor::default(),
);
return_ty =
return_ty.map(|ty| ty.apply_type_mapping(db, &TypeMapping::BindSelf(self_type)));
}
@@ -1232,12 +1248,17 @@ impl<'db> Parameters<'db> {
)
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
fn apply_type_mapping_impl<'a>(
&self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
Self {
value: self
.value
.iter()
.map(|param| param.apply_type_mapping(db, type_mapping))
.map(|param| param.apply_type_mapping_impl(db, type_mapping, visitor))
.collect(),
is_gradual: self.is_gradual,
}
@@ -1416,12 +1437,17 @@ impl<'db> Parameter<'db> {
}
}
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
fn apply_type_mapping_impl<'a>(
&self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
Self {
annotated_type: self
.annotated_type
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
kind: self.kind.apply_type_mapping(db, type_mapping),
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)),
kind: self.kind.apply_type_mapping_impl(db, type_mapping, visitor),
form: self.form,
}
}
@@ -1625,24 +1651,29 @@ pub(crate) enum ParameterKind<'db> {
}
impl<'db> ParameterKind<'db> {
fn apply_type_mapping<'a>(&self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
fn apply_type_mapping_impl<'a>(
&self,
db: &'db dyn Db,
type_mapping: &TypeMapping<'a, 'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
match self {
Self::PositionalOnly { default_type, name } => Self::PositionalOnly {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)),
name: name.clone(),
},
Self::PositionalOrKeyword { default_type, name } => Self::PositionalOrKeyword {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)),
name: name.clone(),
},
Self::KeywordOnly { default_type, name } => Self::KeywordOnly {
default_type: default_type
.as_ref()
.map(|ty| ty.apply_type_mapping(db, type_mapping)),
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, visitor)),
name: name.clone(),
},
Self::Variadic { .. } | Self::KeywordVariadic { .. } => self.clone(),

View File

@@ -212,6 +212,12 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::TypeAlias(_), _) => Ordering::Less,
(_, Type::TypeAlias(_)) => Ordering::Greater,
(Type::TypedDict(left), Type::TypedDict(right)) => {
left.defining_class().cmp(&right.defining_class())
}
(Type::TypedDict(_), _) => Ordering::Less,
(_, Type::TypedDict(_)) => Ordering::Greater,
(Type::Union(_), _) | (_, Type::Union(_)) => {
unreachable!("our type representation does not permit nested unions");
}
@@ -243,12 +249,6 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
unreachable!("Two equal, normalized intersections should share the same Salsa ID")
}
(Type::TypedDict(left), Type::TypedDict(right)) => {
left.defining_class().cmp(&right.defining_class())
}
(Type::TypedDict(_), _) => Ordering::Less,
(_, Type::TypedDict(_)) => Ordering::Greater,
}
}

View File

@@ -48,7 +48,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
"Unpacking target must be a list or tuple expression"
);
let value_type = infer_expression_types(self.db(), value.expression())
let value_type = infer_expression_types(self.db(), value.expression(), false)
.expression_type(value.expression().node_ref(self.db(), self.module()));
let value_type = match value.kind() {

View File

@@ -1 +1 @@
91e2ed0953592795fd8c29e3005a1315bf652ffc
2480d7e7c74493a024eaf254c5d2c6f452c80ee2

View File

@@ -579,7 +579,7 @@ class Distribution(_distribution_parent):
"""Return the 'Name' metadata for the distribution package."""
if sys.version_info >= (3, 13):
@property
def origin(self) -> types.SimpleNamespace: ...
def origin(self) -> types.SimpleNamespace | None: ...
class DistributionFinder(MetaPathFinder):
"""

View File

@@ -257,7 +257,7 @@ class _BaseNetwork(_IPAddressBase, Generic[_A]):
"""
def hosts(self) -> Iterator[_A]:
def hosts(self) -> Iterator[_A] | list[_A]:
"""Generate Iterator over usable hosts in a network.
This is like __iter__ except it doesn't return the network

View File

@@ -13,12 +13,13 @@ of the widgets appearance lies at Themes.
"""
import _tkinter
import sys
import tkinter
from _typeshed import Incomplete, MaybeNone
from collections.abc import Callable
from _typeshed import MaybeNone
from collections.abc import Callable, Iterable
from tkinter.font import _FontDescription
from typing import Any, Literal, TypedDict, overload, type_check_only
from typing_extensions import TypeAlias
from typing_extensions import Never, TypeAlias, Unpack
__all__ = [
"Button",
@@ -53,7 +54,7 @@ def tclobjs_to_py(adict: dict[Any, Any]) -> dict[Any, Any]:
objects.
"""
def setup_master(master=None):
def setup_master(master: tkinter.Misc | None = None):
"""If master is not None, itself is returned. If master is None,
the default master is returned if there is one, otherwise a new
master is created and returned.
@@ -73,13 +74,80 @@ _Padding: TypeAlias = (
# from ttk_widget (aka ttk::widget) manual page, differs from tkinter._Compound
_TtkCompound: TypeAlias = Literal["", "text", "image", tkinter._Compound]
# Last item (option value to apply) varies between different options so use Any.
# It could also be any iterable with items matching the tuple, but that case
# hasn't been added here for consistency with _Padding above.
_Statespec: TypeAlias = tuple[Unpack[tuple[str, ...]], Any]
_ImageStatespec: TypeAlias = tuple[Unpack[tuple[str, ...]], tkinter._ImageSpec]
_VsapiStatespec: TypeAlias = tuple[Unpack[tuple[str, ...]], int]
class _Layout(TypedDict, total=False):
side: Literal["left", "right", "top", "bottom"]
sticky: str # consists of letters 'n', 's', 'w', 'e', may contain repeats, may be empty
unit: Literal[0, 1] | bool
children: _LayoutSpec
# Note: there seem to be some other undocumented keys sometimes
# This could be any sequence when passed as a parameter but will always be a list when returned.
_LayoutSpec: TypeAlias = list[tuple[str, _Layout | None]]
# Keep these in sync with the appropriate methods in Style
class _ElementCreateImageKwargs(TypedDict, total=False):
border: _Padding
height: tkinter._ScreenUnits
padding: _Padding
sticky: str
width: tkinter._ScreenUnits
_ElementCreateArgsCrossPlatform: TypeAlias = (
# Could be any sequence here but types are not homogenous so just type it as tuple
tuple[Literal["image"], tkinter._ImageSpec, Unpack[tuple[_ImageStatespec, ...]], _ElementCreateImageKwargs]
| tuple[Literal["from"], str, str]
| tuple[Literal["from"], str] # (fromelement is optional)
)
if sys.platform == "win32" and sys.version_info >= (3, 13):
class _ElementCreateVsapiKwargsPadding(TypedDict, total=False):
padding: _Padding
class _ElementCreateVsapiKwargsMargin(TypedDict, total=False):
padding: _Padding
class _ElementCreateVsapiKwargsSize(TypedDict):
width: tkinter._ScreenUnits
height: tkinter._ScreenUnits
_ElementCreateVsapiKwargsDict: TypeAlias = (
_ElementCreateVsapiKwargsPadding | _ElementCreateVsapiKwargsMargin | _ElementCreateVsapiKwargsSize
)
_ElementCreateArgs: TypeAlias = ( # noqa: Y047 # It doesn't recognise the usage below for whatever reason
_ElementCreateArgsCrossPlatform
| tuple[Literal["vsapi"], str, int, _ElementCreateVsapiKwargsDict]
| tuple[Literal["vsapi"], str, int, _VsapiStatespec, _ElementCreateVsapiKwargsDict]
)
else:
_ElementCreateArgs: TypeAlias = _ElementCreateArgsCrossPlatform
_ThemeSettingsValue = TypedDict(
"_ThemeSettingsValue",
{
"configure": dict[str, Any],
"map": dict[str, Iterable[_Statespec]],
"layout": _LayoutSpec,
"element create": _ElementCreateArgs,
},
total=False,
)
_ThemeSettings: TypeAlias = dict[str, _ThemeSettingsValue]
class Style:
"""Manipulate style database."""
master: Incomplete
master: tkinter.Misc
tk: _tkinter.TkappType
def __init__(self, master: tkinter.Misc | None = None) -> None: ...
def configure(self, style, query_opt=None, **kw):
# For these methods, values given vary between options. Returned values
# seem to be str, but this might not always be the case.
@overload
def configure(self, style: str) -> dict[str, Any] | None: # Returns None if no configuration.
"""Query or sets the default value of the specified option(s) in
style.
@@ -87,7 +155,12 @@ class Style:
a sequence identifying the value for that option.
"""
def map(self, style, query_opt=None, **kw):
@overload
def configure(self, style: str, query_opt: str, **kw: Any) -> Any: ...
@overload
def configure(self, style: str, query_opt: None = None, **kw: Any) -> None: ...
@overload
def map(self, style: str, query_opt: str) -> _Statespec:
"""Query or sets dynamic values of the specified option(s) in
style.
@@ -97,7 +170,9 @@ class Style:
one or more states and then a value.
"""
def lookup(self, style, option, state=None, default=None):
@overload
def map(self, style: str, query_opt: None = None, **kw: Iterable[_Statespec]) -> dict[str, _Statespec]: ...
def lookup(self, style: str, option: str, state: Iterable[str] | None = None, default: Any | None = None) -> Any:
"""Returns the value specified for option in style.
If state is specified it is expected to be a sequence of one
@@ -105,7 +180,8 @@ class Style:
a fallback value in case no specification for option is found.
"""
def layout(self, style, layoutspec=None):
@overload
def layout(self, style: str, layoutspec: _LayoutSpec) -> list[Never]: # Always seems to return an empty list
"""Define the widget layout for given style. If layoutspec is
omitted, return the layout specification for given style.
@@ -139,16 +215,77 @@ class Style:
is a LAYOUT.
"""
def element_create(self, elementname, etype, *args, **kw) -> None:
@overload
def layout(self, style: str, layoutspec: None = None) -> _LayoutSpec: ...
@overload
def element_create(
self,
elementname: str,
etype: Literal["image"],
default_image: tkinter._ImageSpec,
/,
*imagespec: _ImageStatespec,
border: _Padding = ...,
height: tkinter._ScreenUnits = ...,
padding: _Padding = ...,
sticky: str = ...,
width: tkinter._ScreenUnits = ...,
) -> None:
"""Create a new element in the current theme of given etype."""
def element_names(self):
@overload
def element_create(self, elementname: str, etype: Literal["from"], themename: str, fromelement: str = ..., /) -> None: ...
if sys.platform == "win32" and sys.version_info >= (3, 13): # and tk version >= 8.6
# margin, padding, and (width + height) are mutually exclusive. width
# and height must either both be present or not present at all. Note:
# There are other undocumented options if you look at ttk's source code.
@overload
def element_create(
self,
elementname: str,
etype: Literal["vsapi"],
class_: str,
part: int,
vs_statespec: _VsapiStatespec = ...,
/,
*,
padding: _Padding = ...,
) -> None:
"""Create a new element in the current theme of given etype."""
@overload
def element_create(
self,
elementname: str,
etype: Literal["vsapi"],
class_: str,
part: int,
vs_statespec: _VsapiStatespec = ...,
/,
*,
margin: _Padding = ...,
) -> None: ...
@overload
def element_create(
self,
elementname: str,
etype: Literal["vsapi"],
class_: str,
part: int,
vs_statespec: _VsapiStatespec = ...,
/,
*,
width: tkinter._ScreenUnits,
height: tkinter._ScreenUnits,
) -> None: ...
def element_names(self) -> tuple[str, ...]:
"""Returns the list of elements defined in the current theme."""
def element_options(self, elementname):
def element_options(self, elementname: str) -> tuple[str, ...]:
"""Return the list of elementname's options."""
def theme_create(self, themename, parent=None, settings=None) -> None:
def theme_create(self, themename: str, parent: str | None = None, settings: _ThemeSettings | None = None) -> None:
"""Creates a new theme.
It is an error if themename already exists. If parent is
@@ -157,7 +294,7 @@ class Style:
they are expected to have the same syntax used for theme_settings.
"""
def theme_settings(self, themename, settings) -> None:
def theme_settings(self, themename: str, settings: _ThemeSettings) -> None:
"""Temporarily sets the current theme to themename, apply specified
settings and then restore the previous theme.
@@ -1148,7 +1285,7 @@ class Panedwindow(Widget, tkinter.PanedWindow):
@overload
def config(self, cnf: str) -> tuple[str, str, str, Any, Any]: ...
forget: Incomplete
forget = tkinter.PanedWindow.forget
def insert(self, pos, child, **kw) -> None:
"""Inserts a pane at the specified positions.

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.2
ruff==0.12.10
ruff==0.12.11
mkdocs==1.6.1
mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@39da7a5e761410349e9a1b8abf593b0cdd5453ff
mkdocs-redirects==1.2.2

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.2
ruff==0.12.10
ruff==0.12.11
mkdocs==1.6.1
mkdocs-material==9.5.38
mkdocs-redirects==1.2.2

View File

@@ -152,13 +152,16 @@ class FuzzResult:
def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult:
"""Return a `FuzzResult` instance describing the fuzzing result from this seed."""
# TODO(carljm) debug slowness of this seed
skip_check = seed in {208}
code = generate_random_code(seed)
bug_found = False
minimizer_callback: Callable[[str], bool] | None = None
if args.baseline_executable_path is None:
only_new_bugs = False
if contains_bug(
if not skip_check and contains_bug(
code, executable=args.executable, executable_path=args.test_executable_path
):
bug_found = True
@@ -169,7 +172,7 @@ def fuzz_code(seed: Seed, args: ResolvedCliArgs) -> FuzzResult:
)
else:
only_new_bugs = True
if contains_new_bug(
if not skip_check and contains_new_bug(
code,
executable=args.executable,
test_executable_path=args.test_executable_path,