Compare commits

...

48 Commits

Author SHA1 Message Date
David Peter
048182635a [ty] Use full project names in good.txt 2025-09-24 09:09:47 +02:00
Alex Waygood
09f570af92 Bump mypy_primer pin (#20540) 2025-09-23 19:35:49 +00:00
Shunsuke Shibayama
722f1a7d7a [ty] fix stack overflow when comparing recursive NamedTuple types with is_disjoint_from (#20538)
## Summary

I found this bug while working on #20528.
The minimum reproducible code is:

```python
from __future__ import annotations

from typing import NamedTuple
from ty_extensions import is_disjoint_from, static_assert

class Path(NamedTuple):
    prev: Path | None
    key: str

static_assert(not is_disjoint_from(Path, Path))
```

A stack overflow occurs when a nominal instance type inherits from
`NamedTuple` and is defined recursively.
This PR fixes this bug.

## Test Plan

mdtest updated
2025-09-23 19:29:03 +02:00
ShikChen
dbc5983503 Update import path to ruff-wasm-web (#20539) 2025-09-23 16:57:26 +00:00
Dan Parizher
46decd4feb [pyupgrade] Fix UP008 to not apply when __class__ is a local variable (UP008) (#20497)
## Summary

Fixes #20491
2025-09-23 10:56:39 -04:00
Renkai Ge
bf38e69870 [ty] Rename "possibly unbound" diagnostics to "possibly missing" (#20492)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-09-23 14:26:55 +00:00
fgiacome
4ed8c65d29 [ty] Add positional-only-parameter-as-kwarg error (#20495) 2025-09-23 15:10:45 +01:00
Dan Parizher
2c916562ba [playground] Allow hover quick fixes to appear for overlapping diagnostics (#20527) 2025-09-23 15:15:31 +02:00
Pieter Cardillo Kwok
edb920b4d5 [flake8-async] Implement blocking-path-method (ASYNC240) (#20264)
## Summary
Adds a new rule to find and report use of `os.path` or `pathlib.Path` in
async functions.

Issue: #8451

## Test Plan

Using `cargo insta test`
2025-09-23 08:30:47 -04:00
Dan Parizher
346842f003 [pyflakes] Fix false positives for __annotate__ (Py3.14+) and __warningregistry__ (F821) (#20154)
## Summary

Fixes #19970

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-09-23 08:16:00 -04:00
David Peter
742f8a4ee6 [ty] Use C[T] instead of C[Unknown] for the upper bound of Self (#20479)
### Summary

This PR includes two changes, both of which are necessary to resolve
https://github.com/astral-sh/ty/issues/1196:

* For a generic class `C[T]`, we previously used `C[Unknown]` as the
upper bound of the `Self` type variable. There were two problems with
this. For one, when `Self` appeared in contravariant position, we would
materialize its upper bound to `Bottom[C[Unknown]]` (which might
simplify to `C[Never]` if `C` is covariant in `T`) when accessing
methods on `Top[C[Unknown]]`. This would result in `invalid-argument`
errors on the `self` parameter. Also, using an upper bound of
`C[Unknown]` would mean that inside methods, references to `T` would be
treated as `Unknown`. This could lead to false negatives. To fix this,
we now use `C[T]` (with a "nested" typevar) as the upper bound for
`Self` on `C[T]`.
* In order to make this work, we needed to allow assignability/subtyping
of inferable typevars to other types, since we now check assignability
of e.g. `C[int]` to `C[T]` (when checking assignability to the upper
bound of `Self`) when calling an instance-method on `C[int]` whose
`self` parameter is annotated as `self: Self` (or implicitly `Self`,
following https://github.com/astral-sh/ruff/pull/18007).

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


### Test Plan

Regression tests for both issues.
2025-09-23 14:02:25 +02:00
Matthew Mckee
fd5c48c539 [ty] Add support for inlay hints on attribute assignment (#20485) 2025-09-23 13:14:46 +02:00
justin
ef4df34652 [ty] implement auto() for StrEnum (#20524)
## Summary
see discussion here:
https://github.com/astral-sh/ty/issues/876#issuecomment-3310130167

https://docs.python.org/3/library/enum.html#enum.StrEnum

> Note Using
[auto](https://docs.python.org/3/library/enum.html#enum.auto) with
[StrEnum](https://docs.python.org/3/library/enum.html#enum.StrEnum)
results in the lower-cased member name as the value.

## Test Plan
- new mdtest
- also, added a test to assert the (already correct) behavior for
`IntEnum`

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-09-23 12:22:59 +02:00
Manuel Mendez
036f3616a1 [ty] Add PYTHONPATH to EnvVars and fix on Windows (#20490)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-23 08:27:05 +00:00
Matthew Mckee
68ae9c8a15 [ty] Fix class literal subtyping with object fallback (#20521)
## Summary

@ibraheemdev notes this example failed

```py
from typing import Callable

class X:
    ...

def f(callable: Callable[[], X]) -> X:
    return callable()

x = f(X)
```

Resolves https://github.com/astral-sh/ty/issues/1210

The issue was that we set the `Self` to the class type instead of the
instance type of the class.

## Test Plan

Fix tests in `is_subtype_of.md`
2025-09-22 17:26:25 -07:00
Dan Parizher
094bf70a60 [flake8-bultins] Detect class-scope builtin shadowing in decorators, default args, and attribute initializers (A003) (#20178)
## Summary
Fix #20171

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-09-22 18:12:45 -04:00
Ibraheem Ahmed
32d00cd569 update get-size2 to 0.7.0 2025-09-22 17:37:46 -04:00
David Peter
00a9e65d00 Fix 'cargo shear' runs (#20514)
## Summary

Previous error:

```
▶ cargo shear
Analyzing /home/shark/ruff

ruff_diagnostics -- crates/ruff_diagnostics/Cargo.toml:
  get-size2

ruff_index -- crates/ruff_index/Cargo.toml:
  get-size2

ruff_source_file -- crates/ruff_source_file/Cargo.toml:
  get-size2

ruff_text_size -- crates/ruff_text_size/Cargo.toml:
  get-size2

ty_ide -- crates/ty_ide/Cargo.toml:
  get-size2

ty_project -- crates/ty_project/Cargo.toml:
  get-size2


cargo-shear may have detected unused dependencies incorrectly due to its limitations.
They can be ignored by adding the crate name to the package's Cargo.toml:

[package.metadata.cargo-shear]
ignored = ["crate-name"]

or in the workspace Cargo.toml:

[workspace.metadata.cargo-shear]
ignored = ["crate-name"]
```
2025-09-22 16:04:56 +02:00
Micha Reiser
0c7cfd2a8d Update transitive dependencies (#20513) 2025-09-22 12:50:53 +02:00
renovate[bot]
61bb2a8245 Update Rust crate anyhow to v1.0.100 (#20499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-22 09:51:52 +02:00
Alex Waygood
f1aacd0f2c [ty] The runtime object typing.Protocol is an instance of _ProtocolMeta (#20488)
## Summary

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

This bug doesn't currently cause us any real-world issues, because we
don't yet understand the signatures typeshed gives us for `isinstance()`
and `issubclass()` (typeshed's annotations there use PEP-613 type
aliases). #20107 demonstrates that this will start causing us issues as
soon as we add support for PEP-613 aliases, however, so it makes sense
to fix it now.

## Test Plan

Added mdtests
2025-09-22 08:29:03 +01:00
renovate[bot]
3033d1e5a5 Update Rust crate wasm-bindgen-test to v0.3.53 (#20506)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[wasm-bindgen-test](https://redirect.github.com/wasm-bindgen/wasm-bindgen)
| workspace.dependencies | patch | `0.3.51` -> `0.3.53` |

---

> [!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:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:06:31 +02:00
renovate[bot]
d96d40ef42 Update Rust crate toml to v0.9.7 (#20505)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [toml](https://redirect.github.com/toml-rs/toml) |
workspace.dependencies | patch | `0.9.5` -> `0.9.7` |

---

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

---

### Release Notes

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

###
[`v0.9.7`](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.6...toml-v0.9.7)

[Compare
Source](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.6...toml-v0.9.7)

###
[`v0.9.6`](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.5...toml-v0.9.6)

[Compare
Source](https://redirect.github.com/toml-rs/toml/compare/toml-v0.9.5...toml-v0.9.6)

</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.

---

- [x] <!-- 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:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:04:58 +02:00
renovate[bot]
79224cc53d Update Rust crate clap to v4.5.48 (#20500)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

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

---

> [!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.48`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4548---2025-09-19)

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

##### Documentation

- Add a new CLI Concepts document as another way of framing clap
- Expand the `typed_derive` cookbook entry

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:03:53 +02:00
renovate[bot]
fe01a5e032 Update Rust crate serde to v1.0.226 (#20503)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://redirect.github.com/serde-rs/serde)) |
workspace.dependencies | patch | `1.0.223` -> `1.0.226` |

---

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

---

### Release Notes

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

###
[`v1.0.226`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.226)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.225...v1.0.226)

- Deduplicate variant matching logic inside generated Deserialize impl
for adjacently tagged enums
([#&#8203;2935](https://redirect.github.com/serde-rs/serde/issues/2935),
thanks [@&#8203;Mingun](https://redirect.github.com/Mingun))

###
[`v1.0.225`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.225)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.224...v1.0.225)

- Avoid triggering a deprecation warning in derived Serialize and
Deserialize impls for a data structure that contains its own
deprecations
([#&#8203;2879](https://redirect.github.com/serde-rs/serde/issues/2879),
thanks [@&#8203;rcrisanti](https://redirect.github.com/rcrisanti))

###
[`v1.0.224`](https://redirect.github.com/serde-rs/serde/releases/tag/v1.0.224)

[Compare
Source](https://redirect.github.com/serde-rs/serde/compare/v1.0.223...v1.0.224)

- Remove private types being suggested in rustc diagnostics
([#&#8203;2979](https://redirect.github.com/serde-rs/serde/issues/2979))

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:03:28 +02:00
renovate[bot]
740425d39d Update Rust crate serde_with to v3.14.1 (#20504)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_with](https://redirect.github.com/jonasbb/serde_with) |
workspace.dependencies | patch | `3.14.0` -> `3.14.1` |

---

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

---

### Release Notes

<details>
<summary>jonasbb/serde_with (serde_with)</summary>

###
[`v3.14.1`](https://redirect.github.com/jonasbb/serde_with/releases/tag/v3.14.1):
serde_with v3.14.1

[Compare
Source](https://redirect.github.com/jonasbb/serde_with/compare/v3.14.0...v3.14.1)

##### Fixed

- Show macro expansion in the docs.rs generated rustdoc.
Since macros are used to generate trait implementations, this is useful
to understand the exact generated code.

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:03:10 +02:00
renovate[bot]
4fddd373aa Update Rust crate ordermap to v0.5.12 (#20502)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ordermap](https://redirect.github.com/indexmap-rs/ordermap) |
workspace.dependencies | patch | `0.5.10` -> `0.5.12` |

---

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

---

### Release Notes

<details>
<summary>indexmap-rs/ordermap (ordermap)</summary>

###
[`v0.5.12`](https://redirect.github.com/indexmap-rs/ordermap/blob/HEAD/RELEASES.md#0512-2025-09-15)

[Compare
Source](https://redirect.github.com/indexmap-rs/ordermap/compare/0.5.11...0.5.12)

- Make the minimum `serde` version only apply when "serde" is enabled.

###
[`v0.5.11`](https://redirect.github.com/indexmap-rs/ordermap/blob/HEAD/RELEASES.md#0511-2025-09-15)

[Compare
Source](https://redirect.github.com/indexmap-rs/ordermap/compare/0.5.10...0.5.11)

- Switched the "serde" feature to depend on `serde_core`, improving
build
parallelism in cases where other dependents have enabled "serde/derive".

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:02:58 +02:00
renovate[bot]
0b1e12f086 Update Rust crate indexmap to v2.11.4 (#20501) 2025-09-22 09:02:43 +02:00
renovate[bot]
d12324f06e Update dependency ruff to v0.13.1 (#20498) 2025-09-22 09:02:05 +02:00
Manuel Mendez
2c6c3e78f6 [ty] Search PYTHONPATH to find modules (#20441)
Co-authored-by: Nate Lust <natelust@linux.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-20 13:40:10 +02:00
Micha Reiser
3ffe56d19d [ty] Remove unnecessary FileScopeId to ScopeId conversion (#20481) 2025-09-20 11:20:10 +00:00
GF
eb354608d2 [ty] Add LSP debug information command (#20379)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-20 11:15:13 +00:00
Ibraheem Ahmed
12086dfa69 re-infer RHS of annotated assignments in isolation for assignability diagnostics 2025-09-19 17:00:37 -04:00
Ibraheem Ahmed
5f294f9f2e use type context for inference of generic function calls 2025-09-19 17:00:37 -04:00
Gary Yendell
44fc87f491 [ruff] Add logging-eager-conversion (RUF065) (#19942)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

Fixes #12734

I have started with simply checking if any arguments that are providing
extra values to the log message are calls to `str` or `repr`, as
suggested in the linked issue. There was a concern that this could cause
false positives and the check should be more explicit. I am happy to
look into that if I have some further examples to work with.

If this is the accepted solution then there are more cases to add to the
test and it should possibly also do test for the same behavior via the
`extra` keyword.

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

I have added a new test case and python file to flake8_logging_format
with examples of this anti-pattern.

<!-- How was it tested? -->
2025-09-19 16:43:44 -04:00
Takayuki Maeda
43cda2dfe9 [ruff] Fix B004 to skip invalid hasattr/getattr calls (#20486)
## Summary

Fixes #20440

Fix B004 to skip invalid hasattr/getattr calls

- Add argument validation for `hasattr` and `getattr`
- Skip B004 rule when function calls have invalid argument patterns
2025-09-19 13:44:42 -05:00
Takayuki Maeda
bd5b3e4f6e Deduplicate input paths (#20105)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Fixes #20035, fixes #19395

This is for deduplicating input paths to avoid processing the same file
multiple times.

This is my first contribution, so I'm sorry if I miss something. Please
tell me if this is needed for this feature.

## Test Plan

<!-- How was it tested? -->

I just added a test `find_python_files_deduplicated` in
eee1020e32/crates/ruff_workspace/src/resolver.rs (L1017)
. This pull request adds changes to `WalkPythonFilesState::finish`,
which is used in `python_files_in_path`, so they affect some commands
such as `analyze`, `format`, `check` and so on. I will add snapshot
tests for them if necessary.

I’ve already confirmed that the same thing happens with ruff check as
well.

```
$ echo "x   = 1" > example/foo.py
$ uvx ruff check example example/foo.py
I002 [*] Missing required import: `from __future__ import annotations`
--> /path/to/example/foo.py:1:1
help: Insert required import: `from __future__ import annotations`

I002 [*] Missing required import: `from __future__ import annotations`
--> /path/to/example/foo.py:1:1
help: Insert required import: `from __future__ import annotations`

Found 2 errors.
[*] 2 fixable with the `--fix` option.
```
2025-09-19 14:40:23 -04:00
Andrew Gallant
3bf4dae452 [ty] Make auto-import work in the playground
It turned out that we weren't quite funneling the new completion data
all the way through.

I followed the docs for [`CompletionItem`] for the Monaco editor. It's
similar, but not identical, to the LSP protocol specification.

[`CompletionItem`]: https://microsoft.github.io/monaco-editor/typedoc/interfaces/languages.CompletionItem.html
2025-09-19 14:35:51 -04:00
Takayuki Maeda
8eeca023d6 [ruff] FURB164 Replace -nan with nan when using the value to construct Decimal (#20391)
## Summary

Fixes #19699

Normalize `Decimal(float("-nan"))` to `Decimal("nan")`.

The same handling is implemented in:

c3e873dd82/crates/ruff_linter/src/rules/refurb/rules/verbose_decimal_constructor.rs (L165)
2025-09-19 13:04:29 -05:00
Dan Parizher
c94ddb590f [flake8-bugbear] Add B912: map() without an explicit strict= parameter (#20429)
## Summary

Implements new rule `B912` that requires the `strict=` argument for
`map(...)` calls with two or more iterables on Python 3.14+, following
the same pattern as `B905` for `zip()`.

Closes #20057

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-09-19 12:54:44 -05:00
renovate[bot]
bae8ddfb8a Update Rust crate hashbrown to 0.16.0 (#20399)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [hashbrown](https://redirect.github.com/rust-lang/hashbrown) |
workspace.dependencies | minor | `0.15.0` -> `0.16.0` |

---

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

---

### Release Notes

<details>
<summary>rust-lang/hashbrown (hashbrown)</summary>

###
[`v0.16.0`](https://redirect.github.com/rust-lang/hashbrown/blob/HEAD/CHANGELOG.md#0160---2025-08-28)

[Compare
Source](https://redirect.github.com/rust-lang/hashbrown/compare/v0.15.5...v0.16.0)

##### Changed

- Bump foldhash, the default hasher, to 0.2.0.
- Replaced `DefaultHashBuilder` with a newtype wrapper around `foldhash`
instead
  of re-exporting it directly.

</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:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCJdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: David Peter <mail@david-peter.de>
Co-authored-by: Ibraheem Ahmed <ibraheem@ibraheem.ca>
2025-09-19 13:24:45 -04:00
Dan Parizher
c0fb235a70 [flake8-comprehensions] Preserve trailing commas for single-element lists (C409) (#19571)
## Summary

Fixes #19568
2025-09-19 09:27:14 -04:00
Andrew Gallant
b5a3503a58 [ty] Enable auto-import for completions in WASM builds by default
Now that imports are actually inserted, this should give us some
valuable dog-fooding experience.

Note that we don't currently do any ranking on completions, so until
that is improved, even in-scope completions could suffer. With that
said, this shouldn't have any impact at all in several scenarios (like
completions for attributes on objects).
2025-09-19 08:11:59 -04:00
Andrew Gallant
d45209f425 [ty] Add some tricky test cases for the auto-import importer
We don't attempt to fix these yet. I think there are bigger fish to fry.

I came up with these based on this discussion:
https://github.com/astral-sh/ruff/pull/20439#discussion_r2357769518

Here's one example:

```
if ...:
    from foo import MAGIC
else:
    from bar import MAGIC

MAG<CURSOR>
```

Now in this example, completions will include `MAGIC` from the local
scope. That is, auto-import is involved with that completion. But at
present, auto-import will suggest importing `foo` and `bar` because we
haven't de-duplicated completions yet. Which is fine.

Here's another example:

```
if ...:
    import foo as fubar
else:
    import bar as fubar

MAG<CURSOR>
```

Now here, there is no `MAGIC` symbol in scope. So auto-import is in
play. Let's assume that the user selects `MAGIC` from `foo` in this
example. (`bar` also has `MAGIC`.)

Since we currently ignore the declaration site for symbols with
multiple possible bindings, the importer today doesn't know that
`fubar` _could_ contain `MAGIC`. But even if it did, what would we do
with that information? Should we do this?

```
if ...:
    import foo as fubar
    from foo import MAGIC
else:
    import bar as fubar

MAGIC
```

Or could we reason that `bar` also has `MAGIC`?

```
if ...:
    import foo as fubar
else:
    import bar as fubar

fubar.MAGIC
```

But if we did that, we're making an assumption of user intent, since
they *selected* `foo.MAGIC` but not `bar.MAGIC`.

Anyway, I don't think we need to settle on an answer today, but I
wanted to capture some of these tricky cases in tests at the very
least.
2025-09-19 07:54:07 -04:00
Micha Reiser
5d1cd85662 Shard instrumented benchmark (#20437) 2025-09-19 10:25:04 +02:00
Dhruv Manilawala
902b0b4ce9 [ty] Add support for **kwargs (#20430)
## Summary

This PR adds support for unpacking `**kwargs` argument.

This can be matched against any standard (positional or keyword),
keyword-only, or keyword variadic parameter that haven't been matched
yet.

This PR also takes care of special casing `TypedDict` because the key
names and the corresponding value type is known, so we can be more
precise in our matching and type checking step. In the future, this
special casing would be extended to include `ParamSpec` as well.

Part of astral-sh/ty#247

## Test Plan

Add test cases for various scenarios.
2025-09-19 05:00:30 +00:00
Amethyst Reese
6f2b60708e Exclude snapshots from vscode search results (#20457)
Makes ⌘-T file search ignore snapshot files, so you can actually fuzzy
match "ruff cache" to "ruff/src/cache.rs" without looking/scrolling past
dozens of snapshot files in the search results.
2025-09-18 21:10:42 -07:00
Frazer McLean
bc89d0394c [flake8-simplify] Fix incorrect fix for positive maxsplit without separator (SIM905) (#20056)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

Resolves #20033

## Test Plan

unit tests added to the new split function, existing snapshot test
updated.

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-09-18 20:56:34 +00:00
170 changed files with 4616 additions and 859 deletions

View File

@@ -88,7 +88,6 @@ jobs:
':!crates/ruff_python_formatter/**' \
':!crates/ruff_formatter/**' \
':!crates/ruff_dev/**' \
':!crates/ruff_db/**' \
':scripts/*' \
':python/**' \
':.github/workflows/ci.yaml' \
@@ -907,10 +906,13 @@ jobs:
run: npm run fmt:check
working-directory: playground
benchmarks-instrumented:
benchmarks-instrumented-ruff:
name: "benchmarks instrumented (ruff)"
runs-on: ubuntu-24.04
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
if: |
github.ref == 'refs/heads/main' ||
(needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -930,7 +932,42 @@ jobs:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark formatter lexer linter parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
with:
mode: instrumentation
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
benchmarks-instrumented-ty:
name: "benchmarks instrumented (ty)"
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
timeout-minutes: 20
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
with:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1

View File

@@ -49,7 +49,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@fc0f612798710b0dd69bb7528bc9b361dc60bd43"
ecosystem-analyzer \
--verbose \

View File

@@ -3,4 +3,7 @@
"--all-features"
],
"rust-analyzer.check.command": "clippy",
}
"search.exclude": {
"**/*.snap": true
}
}

370
Cargo.lock generated
View File

@@ -23,12 +23,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -104,9 +98,9 @@ dependencies = [
[[package]]
name = "anstyle-svg"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc03a770ef506fe1396c0e476120ac0e6523cf14b74218dd5f18cd6833326fa9"
checksum = "26b9ec8c976eada1b0f9747a3d7cc4eae3bef10613e443746e7487f26c872fde"
dependencies = [
"anstyle",
"anstyle-lossy",
@@ -128,9 +122,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.99"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "approx"
@@ -284,9 +278,9 @@ dependencies = [
[[package]]
name = "boxcar"
version = "0.2.13"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c4925bc979b677330a8c7fe7a8c94af2dbb4a2d37b4a20a80d884400f46baa"
checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
[[package]]
name = "bstr"
@@ -346,10 +340,11 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.31"
version = "1.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -357,9 +352,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.1"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "cfg_aliases"
@@ -369,14 +364,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.41"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-link 0.1.3",
"windows-link 0.2.0",
]
[[package]]
@@ -408,9 +402,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.47"
version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
dependencies = [
"clap_builder",
"clap_derive",
@@ -418,9 +412,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.47"
version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
dependencies = [
"anstream",
"anstyle",
@@ -431,9 +425,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.55"
version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a"
dependencies = [
"clap",
]
@@ -650,15 +644,15 @@ dependencies = [
[[package]]
name = "console"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.1",
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -837,9 +831,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.20.11"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core",
"darling_macro",
@@ -847,9 +841,9 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.11"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
@@ -861,9 +855,9 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.20.11"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
@@ -956,7 +950,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.61.0",
]
[[package]]
@@ -1037,12 +1031,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.13"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -1053,12 +1047,11 @@ checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6"
[[package]]
name = "escargot"
version = "0.5.14"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f351750780493fc33fa0ce8ba3c7d61f9736cfa3b3bb9ee2342643ffe40211"
checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf"
dependencies = [
"log",
"once_cell",
"serde",
"serde_json",
]
@@ -1101,6 +1094,12 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
[[package]]
name = "flate2"
version = "1.1.2"
@@ -1168,9 +1167,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.6.2"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a17a226478b2e8294ded60782c03efe54476aa8cd1371d0e5ad9d1071e74e0"
checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843"
dependencies = [
"attribute-derive",
"quote",
@@ -1179,21 +1178,21 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.6.2"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5697765925a05c9d401dd04a93dfd662d336cc25fdcc3301220385a1ffcfdde5"
checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed"
dependencies = [
"compact_str",
"get-size-derive2",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"smallvec",
]
[[package]]
name = "getopts"
version = "0.2.23"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width 0.2.1",
]
@@ -1219,7 +1218,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
@@ -1280,6 +1279,15 @@ dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
dependencies = [
"equivalent",
]
[[package]]
name = "hashlink"
version = "0.10.0"
@@ -1321,9 +1329,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.63"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1493,13 +1501,14 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.11.1"
version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"serde",
"serde_core",
]
[[package]]
@@ -1508,7 +1517,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
"console 0.16.0",
"console 0.16.1",
"portable-atomic",
"unicode-width 0.2.1",
"unit-prefix",
@@ -1588,9 +1597,9 @@ dependencies = [
[[package]]
name = "inventory"
version = "0.3.20"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83"
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
dependencies = [
"rustversion",
]
@@ -1719,9 +1728,9 @@ dependencies = [
[[package]]
name = "jobserver"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.3",
"libc",
@@ -1735,9 +1744,9 @@ checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24"
[[package]]
name = "js-sys"
version = "0.3.78"
version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1812,9 +1821,9 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.9.4",
"libc",
@@ -1835,9 +1844,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
@@ -2133,12 +2142,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
version = "0.5.10"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dcd63f1ae4b091e314a26627c467dd8810d674ba798abc0e566679955776c63"
checksum = "b100f7dd605611822d30e182214d3c02fdefce2d801d23993f6b6ba6ca1392af"
dependencies = [
"indexmap",
"serde",
"serde_core",
]
[[package]]
@@ -2289,9 +2299,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8"
dependencies = [
"memchr",
"thiserror 2.0.16",
@@ -2300,9 +2310,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663"
dependencies = [
"pest",
"pest_generator",
@@ -2310,9 +2320,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f"
dependencies = [
"pest",
"pest_meta",
@@ -2323,9 +2333,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420"
dependencies = [
"pest",
"sha2",
@@ -2398,9 +2408,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
@@ -2453,9 +2463,9 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
@@ -2705,15 +2715,15 @@ dependencies = [
[[package]]
name = "regex-lite"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
[[package]]
name = "regex-syntax"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "ron"
@@ -2994,7 +3004,7 @@ dependencies = [
"fern",
"glob",
"globset",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"imperative",
"insta",
"is-macro",
@@ -3427,22 +3437,22 @@ checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08"
[[package]]
name = "rustix"
version = "1.0.8"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags 2.9.4",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
@@ -3537,9 +3547,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.223"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
dependencies = [
"serde_core",
"serde_derive",
@@ -3558,18 +3568,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.223"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.223"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
dependencies = [
"proc-macro2",
"quote",
@@ -3613,11 +3623,11 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.0"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
dependencies = [
"serde",
"serde_core",
]
[[package]]
@@ -3631,9 +3641,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.14.0"
version = "3.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e"
dependencies = [
"serde",
"serde_derive",
@@ -3642,9 +3652,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.14.0"
version = "3.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e"
dependencies = [
"darling",
"proc-macro2",
@@ -3830,7 +3840,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -3844,12 +3854,12 @@ dependencies = [
[[package]]
name = "terminal_size"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -4009,9 +4019,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
@@ -4024,14 +4034,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
dependencies = [
"indexmap",
"serde",
"serde_core",
"serde_spanned",
"toml_datetime 0.7.0",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
@@ -4039,44 +4049,39 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.11"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
version = "0.23.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b"
dependencies = [
"indexmap",
"toml_datetime 0.6.11",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
[[package]]
name = "tracing"
@@ -4286,6 +4291,7 @@ dependencies = [
"tracing",
"ty_combine",
"ty_python_semantic",
"ty_static",
"ty_vendored",
]
@@ -4303,7 +4309,7 @@ dependencies = [
"drop_bomb",
"get-size2",
"glob",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"indexmap",
"insta",
"itertools 0.14.0",
@@ -4513,9 +4519,9 @@ dependencies = [
[[package]]
name = "unicode-id"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
[[package]]
name = "unicode-ident"
@@ -4740,18 +4746,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
version = "0.14.7+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
dependencies = [
"wit-bindgen-rt",
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819"
dependencies = [
"cfg-if",
"once_cell",
@@ -4762,9 +4777,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c"
dependencies = [
"bumpalo",
"log",
@@ -4776,9 +4791,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.51"
version = "0.4.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe"
checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67"
dependencies = [
"cfg-if",
"js-sys",
@@ -4789,9 +4804,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4799,9 +4814,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32"
dependencies = [
"proc-macro2",
"quote",
@@ -4812,18 +4827,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
version = "0.3.51"
version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad"
checksum = "aee0a0f5343de9221a0d233b04520ed8dc2e6728dce180b1dcd9288ec9d9fa3c"
dependencies = [
"js-sys",
"minicov",
@@ -4834,9 +4849,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.51"
version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d"
checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d"
dependencies = [
"proc-macro2",
"quote",
@@ -4845,9 +4860,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.78"
version = "0.3.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12"
checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4885,22 +4900,22 @@ dependencies = [
[[package]]
name = "winapi-util"
version = "0.1.9"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
name = "windows-core"
version = "0.61.2"
version = "0.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-link 0.2.0",
"windows-result",
"windows-strings",
]
@@ -4941,20 +4956,20 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-result"
version = "0.3.4"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
dependencies = [
"windows-link 0.1.3",
"windows-link 0.2.0",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
dependencies = [
"windows-link 0.1.3",
"windows-link 0.2.0",
]
[[package]]
@@ -5124,9 +5139,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
@@ -5138,13 +5153,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.4",
]
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
@@ -5193,18 +5205,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.26"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
@@ -5299,9 +5311,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",

View File

@@ -86,7 +86,7 @@ etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.6.2", features = [
get-size2 = { version = "0.7.0", features = [
"derive",
"smallvec",
"hashbrown",
@@ -95,7 +95,7 @@ get-size2 = { version = "0.6.2", features = [
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
globwalk = { version = "0.9.1" }
hashbrown = { version = "0.15.0", default-features = false, features = [
hashbrown = { version = "0.16.0", default-features = false, features = [
"raw-entry",
"equivalent",
"inline-more",
@@ -203,7 +203,7 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom", "ruff_options_metadata", "uuid"]
ignored = ["getrandom", "ruff_options_metadata", "uuid", "get-size2"]
[workspace.lints.rust]

View File

@@ -500,6 +500,35 @@ OTHER = "OTHER"
Ok(())
}
/// Regression test for <https://github.com/astral-sh/ruff/issues/20035>
#[test]
fn deduplicate_directory_and_explicit_file() -> Result<()> {
let tempdir = TempDir::new()?;
let root = tempdir.path();
let main = root.join("main.py");
fs::write(&main, "x = 1\n")?;
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.current_dir(root)
.args(["format", "--no-cache", "--check"])
.arg(".")
.arg("main.py"),
@r"
success: false
exit_code: 1
----- stdout -----
Would reformat: main.py
1 file would be reformatted
----- stderr -----
"
);
Ok(())
}
#[test]
fn syntax_error() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -271,6 +271,50 @@ OTHER = "OTHER"
Ok(())
}
/// Regression test for <https://github.com/astral-sh/ruff/issues/20035>
#[test]
fn deduplicate_directory_and_explicit_file() -> Result<()> {
let tempdir = TempDir::new()?;
let root = tempdir.path();
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[lint]
exclude = ["main.py"]
"#,
)?;
let main = root.join("main.py");
fs::write(&main, "import os\n")?;
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.current_dir(root)
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.arg(".")
// Explicitly pass main.py, should be linted regardless of it being excluded by lint.exclude
.arg("main.py"),
@r"
success: false
exit_code: 1
----- stdout -----
main.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"
);
});
Ok(())
}
#[test]
fn exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="164px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1356px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="869px" height="236px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<svg width="911px" height="236px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,7 +1,7 @@
<svg width="768px" height="290px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="146px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1196px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="182px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.fg-yellow { fill: #AA5500 }

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="740px" height="128px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +1,7 @@
<svg width="1196px" height="164px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.bg { fill: #000000 }
.fg-bright-blue { fill: #5555FF }
.fg-bright-red { fill: #FF5555 }
.container {

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -232,7 +232,7 @@ static STATIC_FRAME: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLo
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
500,
600,
)
});

View File

@@ -504,8 +504,8 @@ impl ToOwned for SystemPath {
pub struct SystemPathBuf(#[cfg_attr(feature = "schemars", schemars(with = "String"))] Utf8PathBuf);
impl get_size2::GetSize for SystemPathBuf {
fn get_heap_size(&self) -> usize {
self.0.capacity()
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
(self.0.capacity(), tracker)
}
}

View File

@@ -92,8 +92,8 @@ impl ToOwned for VendoredPath {
pub struct VendoredPathBuf(Utf8PathBuf);
impl get_size2::GetSize for VendoredPathBuf {
fn get_heap_size(&self) -> usize {
self.0.capacity()
fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
(self.0.capacity(), tracker)
}
}

View File

@@ -0,0 +1,104 @@
import os
from typing import Optional
from pathlib import Path
## Valid cases:
def os_path_in_foo():
file = "file.txt"
os.path.abspath(file) # OK
os.path.exists(file) # OK
os.path.split() # OK
async def non_io_os_path_methods():
os.path.split() # OK
os.path.dirname() # OK
os.path.basename() # OK
os.path.join() # OK
def pathlib_path_in_foo():
path = Path("src/my_text.txt") # OK
path.exists() # OK
with path.open() as f: # OK
...
path = Path("src/my_text.txt").open() # OK
async def non_io_pathlib_path_methods():
path = Path("src/my_text.txt")
path.is_absolute() # OK
path.is_relative_to() # OK
path.as_posix() # OK
path.relative_to() # OK
def inline_path_method_call():
Path("src/my_text.txt").open() # OK
Path("src/my_text.txt").open().flush() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def trio_path_in_foo():
from trio import Path
path = Path("src/my_text.txt") # OK
await path.absolute() # OK
await path.exists() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def anyio_path_in_foo():
from anyio import Path
path = Path("src/my_text.txt") # OK
await path.absolute() # OK
await path.exists() # OK
with Path("src/my_text.txt").open() as f: # OK
...
async def path_open_in_foo():
path = Path("src/my_text.txt") # OK
path.open() # OK, covered by ASYNC230
## Invalid cases:
async def os_path_in_foo():
file = "file.txt"
os.path.abspath(file) # ASYNC240
os.path.exists(file) # ASYNC240
async def pathlib_path_in_foo():
path = Path("src/my_text.txt")
path.exists() # ASYNC240
async def pathlib_path_in_foo():
import pathlib
path = pathlib.Path("src/my_text.txt")
path.exists() # ASYNC240
async def inline_path_method_call():
Path("src/my_text.txt").exists() # ASYNC240
Path("src/my_text.txt").absolute().exists() # ASYNC240
async def aliased_path_in_foo():
from pathlib import Path as PathAlias
path = PathAlias("src/my_text.txt")
path.exists() # ASYNC240
global_path = Path("src/my_text.txt")
async def global_path_in_foo():
global_path.exists() # ASYNC240
async def path_as_simple_parameter_type(path: Path):
path.exists() # ASYNC240
async def path_as_union_parameter_type(path: Path | None):
path.exists() # ASYNC240
async def path_as_optional_parameter_type(path: Optional[Path]):
path.exists() # ASYNC240

View File

@@ -52,3 +52,21 @@ class A:
assert hasattr(A(), "__call__")
assert callable(A()) is False
# https://github.com/astral-sh/ruff/issues/20440
def test_invalid_hasattr_calls():
hasattr(0, "__call__", 0) # 3 args - invalid
hasattr(0, "__call__", x=0) # keyword arg - invalid
hasattr(0, "__call__", 0, x=0) # 3 args + keyword - invalid
hasattr() # no args - invalid
hasattr(0) # 1 arg - invalid
hasattr(*(), "__call__", "extra") # unpacking - invalid
hasattr(*()) # unpacking - invalid
def test_invalid_getattr_calls():
getattr(0, "__call__", None, "extra") # 4 args - invalid
getattr(0, "__call__", default=None) # keyword arg - invalid
getattr() # no args - invalid
getattr(0) # 1 arg - invalid
getattr(*(), "__call__", None, "extra") # unpacking - invalid
getattr(*()) # unpacking - invalid

View File

@@ -0,0 +1,33 @@
from itertools import count, cycle, repeat
# Errors
map(lambda x: x, [1, 2, 3])
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
# Errors (limited iterators).
map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
import builtins
# Still an error even though it uses the qualified name
builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
# OK
map(lambda x: x, [1, 2, 3], strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=True)
map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True), strict=True)
# OK (single iterable - no strict required)
map(lambda x: x, [1, 2, 3])
# OK (infinite iterators)
map(lambda x, y: x + y, [1, 2, 3], cycle([1, 2, 3]))
map(lambda x, y: x + y, [1, 2, 3], repeat(1))
map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=None))
map(lambda x, y: x + y, [1, 2, 3], count())

View File

@@ -19,3 +19,18 @@ class MyClass:
def attribute_usage(self) -> id:
pass
class C:
@staticmethod
def property(f):
return f
id = 1
@[property][0]
def f(self, x=[id]):
return x
bin = 2
foo = [bin]

View File

@@ -42,3 +42,6 @@ tuple(
x for x in [1,2,3]
}
)
t9 = tuple([1],)
t10 = tuple([1, 2],)

View File

@@ -170,3 +170,4 @@ print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
# leading/trailing whitespace should not count towards maxsplit
" a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
" a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
"a b".split(maxsplit=1) # ["a", "b"]

View File

@@ -287,3 +287,19 @@ class C(B):
def f(self):
C = B # Local variable C shadows the class name
return super(C, self).f() # Should NOT trigger UP008
# See: https://github.com/astral-sh/ruff/issues/20491
# UP008 should not apply when __class__ is a local variable
class A:
def f(self):
return 1
class B(A):
def f(self):
return 2
class C(B):
def f(self):
__class__ = B # Local variable __class__ shadows the implicit __class__
return super(__class__, self).f() # Should NOT trigger UP008

View File

@@ -0,0 +1,39 @@
import logging
# %s + str()
logging.info("Hello %s", str("World!"))
logging.log(logging.INFO, "Hello %s", str("World!"))
# %s + repr()
logging.info("Hello %s", repr("World!"))
logging.log(logging.INFO, "Hello %s", repr("World!"))
# %r + str()
logging.info("Hello %r", str("World!"))
logging.log(logging.INFO, "Hello %r", str("World!"))
# %r + repr()
logging.info("Hello %r", repr("World!"))
logging.log(logging.INFO, "Hello %r", repr("World!"))
from logging import info, log
# %s + str()
info("Hello %s", str("World!"))
log(logging.INFO, "Hello %s", str("World!"))
# %s + repr()
info("Hello %s", repr("World!"))
log(logging.INFO, "Hello %s", repr("World!"))
# %r + str()
info("Hello %r", str("World!"))
log(logging.INFO, "Hello %r", str("World!"))
# %r + repr()
info("Hello %r", repr("World!"))
log(logging.INFO, "Hello %r", repr("World!"))
def str(s): return f"str = {s}"
# Don't flag this
logging.info("Hello %s", str("World!"))

View File

@@ -669,6 +669,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
flake8_async::rules::blocking_open_call(checker, call);
}
if checker.is_rule_enabled(Rule::BlockingPathMethodInAsyncFunction) {
flake8_async::rules::blocking_os_path(checker, call);
}
if checker.any_rule_enabled(&[
Rule::CreateSubprocessInAsyncFunction,
Rule::RunProcessInAsyncFunction,
@@ -717,7 +720,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_bugbear::rules::re_sub_positional_args(checker, call);
}
if checker.is_rule_enabled(Rule::UnreliableCallableCheck) {
flake8_bugbear::rules::unreliable_callable_check(checker, expr, func, args);
flake8_bugbear::rules::unreliable_callable_check(
checker, expr, func, args, keywords,
);
}
if checker.is_rule_enabled(Rule::StripWithMultiCharacters) {
flake8_bugbear::rules::strip_with_multi_characters(checker, expr, func, args);
@@ -741,6 +746,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_bugbear::rules::zip_without_explicit_strict(checker, call);
}
}
if checker.is_rule_enabled(Rule::MapWithoutExplicitStrict) {
if checker.target_version() >= PythonVersion::PY314 {
flake8_bugbear::rules::map_without_explicit_strict(checker, call);
}
}
if checker.is_rule_enabled(Rule::NoExplicitStacklevel) {
flake8_bugbear::rules::no_explicit_stacklevel(checker, call);
}
@@ -1279,6 +1289,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::UnnecessaryEmptyIterableWithinDequeCall) {
ruff::rules::unnecessary_literal_within_deque_call(checker, call);
}
if checker.is_rule_enabled(Rule::LoggingEagerConversion) {
ruff::rules::logging_eager_conversion(checker, call);
}
if checker.is_rule_enabled(Rule::StarmapZip) {
ruff::rules::starmap_zip(checker, call);
}

View File

@@ -56,7 +56,7 @@ use ruff_python_semantic::{
Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel,
SemanticModelFlags, StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins};
use ruff_python_stdlib::builtins::{python_builtins, python_magic_globals};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -2550,7 +2550,7 @@ impl<'a> Checker<'a> {
for builtin in standard_builtins {
bind_builtin(builtin);
}
for builtin in MAGIC_GLOBALS {
for builtin in python_magic_globals(target_version.minor) {
bind_builtin(builtin);
}
for builtin in &settings.builtins {

View File

@@ -341,6 +341,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
(Flake8Async, "230") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOpenCallInAsyncFunction),
(Flake8Async, "240") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingPathMethodInAsyncFunction),
(Flake8Async, "250") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingInputInAsyncFunction),
(Flake8Async, "251") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingSleepInAsyncFunction),
@@ -394,6 +395,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict),
(Flake8Bugbear, "909") => (RuleGroup::Preview, rules::flake8_bugbear::rules::LoopIteratorMutation),
(Flake8Bugbear, "911") => (RuleGroup::Stable, rules::flake8_bugbear::rules::BatchedWithoutExplicitStrict),
(Flake8Bugbear, "912") => (RuleGroup::Preview, rules::flake8_bugbear::rules::MapWithoutExplicitStrict),
// flake8-blind-except
(Flake8BlindExcept, "001") => (RuleGroup::Stable, rules::flake8_blind_except::rules::BlindExcept),
@@ -1051,6 +1053,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises),
(Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict),
(Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions),
(Ruff, "065") => (RuleGroup::Preview, rules::ruff::rules::LoggingEagerConversion),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),

View File

@@ -65,7 +65,7 @@ pub(crate) fn remove_imports<'a>(
if member == "*" {
found_star = true;
} else {
bail!("Expected \"*\" for unused import (got: \"{}\")", member);
bail!("Expected \"*\" for unused import (got: \"{member}\")");
}
}
if !found_star {

View File

@@ -228,3 +228,10 @@ pub(crate) const fn is_sim910_expanded_key_support_enabled(settings: &LinterSett
pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20178
pub(crate) const fn is_a003_class_scope_shadowing_expansion_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}

View File

@@ -28,6 +28,7 @@ mod tests {
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::BlockingOpenCallInAsyncFunction, Path::new("ASYNC230.py"))]
#[test_case(Rule::BlockingPathMethodInAsyncFunction, Path::new("ASYNC240.py"))]
#[test_case(Rule::BlockingInputInAsyncFunction, Path::new("ASYNC250.py"))]
#[test_case(Rule::BlockingSleepInAsyncFunction, Path::new("ASYNC251.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -0,0 +1,246 @@
use crate::Violation;
use crate::checkers::ast::Checker;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_semantic::analyze::typing::{TypeChecker, check_type, traverse_union_and_optional};
use ruff_text_size::Ranged;
/// ## What it does
/// Checks that async functions do not call blocking `os.path` or `pathlib.Path`
/// methods.
///
/// ## Why is this bad?
/// Calling some `os.path` or `pathlib.Path` methods in an async function will block
/// the entire event loop, preventing it from executing other tasks while waiting
/// for the operation. This negates the benefits of asynchronous programming.
///
/// Instead, use the methods' async equivalents from `trio.Path` or `anyio.Path`.
///
/// ## Example
/// ```python
/// import os
///
///
/// async def func():
/// path = "my_file.txt"
/// file_exists = os.path.exists(path)
/// ```
///
/// Use instead:
/// ```python
/// import trio
///
///
/// async def func():
/// path = trio.Path("my_file.txt")
/// file_exists = await path.exists()
/// ```
///
/// Non-blocking methods are OK to use:
/// ```python
/// import pathlib
///
///
/// async def func():
/// path = pathlib.Path("my_file.txt")
/// file_dirname = path.dirname()
/// new_path = os.path.join("/tmp/src/", path)
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct BlockingPathMethodInAsyncFunction {
path_library: String,
}
impl Violation for BlockingPathMethodInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Async functions should not use {path_library} methods, use trio.Path or anyio.path",
path_library = self.path_library
)
}
}
/// ASYNC240
pub(crate) fn blocking_os_path(checker: &Checker, call: &ExprCall) {
let semantic = checker.semantic();
if !semantic.in_async_context() {
return;
}
// Check if an expression is calling I/O related os.path method.
// Just initializing pathlib.Path object is OK, we can return
// early in that scenario.
if let Some(qualified_name) = semantic.resolve_qualified_name(call.func.as_ref()) {
let segments = qualified_name.segments();
if !matches!(segments, ["os", "path", _]) {
return;
}
let Some(os_path_method) = segments.last() else {
return;
};
if maybe_calling_io_operation(os_path_method) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "os.path".to_string(),
},
call.func.range(),
);
}
return;
}
let Some(ast::ExprAttribute { value, attr, .. }) = call.func.as_attribute_expr() else {
return;
};
if !maybe_calling_io_operation(attr.id.as_str()) {
return;
}
// Check if an expression is a pathlib.Path constructor that directly
// calls an I/O method.
if PathlibPathChecker::match_initializer(value, semantic) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "pathlib.Path".to_string(),
},
call.func.range(),
);
return;
}
// Lastly, check if a variable is a pathlib.Path instance and it's
// calling an I/O method.
let Some(name) = value.as_name_expr() else {
return;
};
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return;
};
if check_type::<PathlibPathChecker>(binding, semantic) {
checker.report_diagnostic(
BlockingPathMethodInAsyncFunction {
path_library: "pathlib.Path".to_string(),
},
call.func.range(),
);
}
}
struct PathlibPathChecker;
impl PathlibPathChecker {
fn is_pathlib_path_constructor(
semantic: &ruff_python_semantic::SemanticModel,
expr: &Expr,
) -> bool {
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
return false;
};
matches!(
qualified_name.segments(),
[
"pathlib",
"Path"
| "PosixPath"
| "PurePath"
| "PurePosixPath"
| "PureWindowsPath"
| "WindowsPath"
]
)
}
}
impl TypeChecker for PathlibPathChecker {
fn match_annotation(annotation: &Expr, semantic: &ruff_python_semantic::SemanticModel) -> bool {
if Self::is_pathlib_path_constructor(semantic, annotation) {
return true;
}
let mut found = false;
traverse_union_and_optional(
&mut |inner_expr, _| {
if Self::is_pathlib_path_constructor(semantic, inner_expr) {
found = true;
}
},
semantic,
annotation,
);
found
}
fn match_initializer(
initializer: &Expr,
semantic: &ruff_python_semantic::SemanticModel,
) -> bool {
let Expr::Call(ast::ExprCall { func, .. }) = initializer else {
return false;
};
Self::is_pathlib_path_constructor(semantic, func)
}
}
fn maybe_calling_io_operation(attr: &str) -> bool {
// ".open()" is added to the allow list to let ASYNC 230 handle
// that case.
!matches!(
attr,
"ALLOW_MISSING"
| "altsep"
| "anchor"
| "as_posix"
| "as_uri"
| "basename"
| "commonpath"
| "commonprefix"
| "curdir"
| "defpath"
| "devnull"
| "dirname"
| "drive"
| "expandvars"
| "extsep"
| "genericpath"
| "is_absolute"
| "is_relative_to"
| "is_reserved"
| "isabs"
| "join"
| "joinpath"
| "match"
| "name"
| "normcase"
| "os"
| "open"
| "pardir"
| "parent"
| "parents"
| "parts"
| "pathsep"
| "relative_to"
| "root"
| "samestat"
| "sep"
| "split"
| "splitdrive"
| "splitext"
| "splitroot"
| "stem"
| "suffix"
| "suffixes"
| "supports_unicode_filenames"
| "sys"
| "with_name"
| "with_segments"
| "with_stem"
| "with_suffix"
)
}

View File

@@ -5,6 +5,7 @@ pub(crate) use blocking_http_call::*;
pub(crate) use blocking_http_call_httpx::*;
pub(crate) use blocking_input::*;
pub(crate) use blocking_open_call::*;
pub(crate) use blocking_path_methods::*;
pub(crate) use blocking_process_invocation::*;
pub(crate) use blocking_sleep::*;
pub(crate) use cancel_scope_no_checkpoint::*;
@@ -18,6 +19,7 @@ mod blocking_http_call;
mod blocking_http_call_httpx;
mod blocking_input;
mod blocking_open_call;
mod blocking_path_methods;
mod blocking_process_invocation;
mod blocking_sleep;
mod cancel_scope_no_checkpoint;

View File

@@ -0,0 +1,111 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC240 Async functions should not use os.path methods, use trio.Path or anyio.path
--> ASYNC240.py:67:5
|
65 | file = "file.txt"
66 |
67 | os.path.abspath(file) # ASYNC240
| ^^^^^^^^^^^^^^^
68 | os.path.exists(file) # ASYNC240
|
ASYNC240 Async functions should not use os.path methods, use trio.Path or anyio.path
--> ASYNC240.py:68:5
|
67 | os.path.abspath(file) # ASYNC240
68 | os.path.exists(file) # ASYNC240
| ^^^^^^^^^^^^^^
69 |
70 | async def pathlib_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:72:5
|
70 | async def pathlib_path_in_foo():
71 | path = Path("src/my_text.txt")
72 | path.exists() # ASYNC240
| ^^^^^^^^^^^
73 |
74 | async def pathlib_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:78:5
|
77 | path = pathlib.Path("src/my_text.txt")
78 | path.exists() # ASYNC240
| ^^^^^^^^^^^
79 |
80 | async def inline_path_method_call():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:81:5
|
80 | async def inline_path_method_call():
81 | Path("src/my_text.txt").exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 | Path("src/my_text.txt").absolute().exists() # ASYNC240
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:82:5
|
80 | async def inline_path_method_call():
81 | Path("src/my_text.txt").exists() # ASYNC240
82 | Path("src/my_text.txt").absolute().exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83 |
84 | async def aliased_path_in_foo():
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:88:5
|
87 | path = PathAlias("src/my_text.txt")
88 | path.exists() # ASYNC240
| ^^^^^^^^^^^
89 |
90 | global_path = Path("src/my_text.txt")
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:93:5
|
92 | async def global_path_in_foo():
93 | global_path.exists() # ASYNC240
| ^^^^^^^^^^^^^^^^^^
94 |
95 | async def path_as_simple_parameter_type(path: Path):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:96:5
|
95 | async def path_as_simple_parameter_type(path: Path):
96 | path.exists() # ASYNC240
| ^^^^^^^^^^^
97 |
98 | async def path_as_union_parameter_type(path: Path | None):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:99:5
|
98 | async def path_as_union_parameter_type(path: Path | None):
99 | path.exists() # ASYNC240
| ^^^^^^^^^^^
100 |
101 | async def path_as_optional_parameter_type(path: Optional[Path]):
|
ASYNC240 Async functions should not use pathlib.Path methods, use trio.Path or anyio.path
--> ASYNC240.py:102:5
|
101 | async def path_as_optional_parameter_type(path: Optional[Path]):
102 | path.exists() # ASYNC240
| ^^^^^^^^^^^
|

View File

@@ -4,6 +4,7 @@ use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use ruff_python_ast::{self as ast, Arguments, Expr};
/// Return `true` if the statement containing the current expression is the last
/// top-level expression in the cell. This assumes that the source is a Jupyter
@@ -27,3 +28,54 @@ pub(super) fn at_last_top_level_expression_in_cell(
.all(|token| token.kind() == SimpleTokenKind::Semi || token.kind().is_trivia())
})
}
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar).
pub(crate) fn is_infinite_iterable(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
}) = &arg
else {
return false;
};
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| match qualified_name.segments() {
["itertools", "cycle" | "count"] => true,
["itertools", "repeat"] => {
// Ex) `itertools.repeat(1)`
if keywords.is_empty() && args.len() == 1 {
return true;
}
// Ex) `itertools.repeat(1, None)`
if args.len() == 2 && args[1].is_none_literal_expr() {
return true;
}
// Ex) `itertools.repeat(1, times=None)`
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|name| name == "times")
&& keyword.value.is_none_literal_expr()
{
return true;
}
}
false
}
_ => false,
})
}
/// Return `true` if any expression in the iterator appears to be an infinite iterator.
pub(crate) fn any_infinite_iterables<'a>(
iter: impl IntoIterator<Item = &'a Expr>,
semantic: &SemanticModel,
) -> bool {
iter.into_iter()
.any(|arg| is_infinite_iterable(arg, semantic))
}

View File

@@ -16,6 +16,7 @@ mod tests {
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::settings::types::PreviewMode;
use ruff_python_ast::PythonVersion;
#[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))]
@@ -81,11 +82,35 @@ mod tests {
Ok(())
}
#[test_case(Rule::MapWithoutExplicitStrict, Path::new("B912.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
unresolved_target_version: PythonVersion::PY314.into(),
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(
Rule::ClassAsDataStructure,
Path::new("class_as_data_structure.py"),
PythonVersion::PY39
)]
#[test_case(
Rule::MapWithoutExplicitStrict,
Path::new("B912.py"),
PythonVersion::PY313
)]
fn rules_with_target_version(
rule_code: Rule,
path: &Path,

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::ExprCall;
use ruff_python_ast::PythonVersion;
use crate::checkers::ast::Checker;
use crate::rules::flake8_bugbear::rules::is_infinite_iterable;
use crate::rules::flake8_bugbear::helpers::is_infinite_iterable;
use crate::{FixAvailability, Violation};
/// ## What it does

View File

@@ -0,0 +1,89 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
/// Checks for `map` calls without an explicit `strict` parameter when called with two or more iterables.
///
/// This rule applies to Python 3.14 and later, where `map` accepts a `strict` keyword
/// argument. For details, see: [Whats New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html).
///
/// ## Why is this bad?
/// By default, if the iterables passed to `map` are of different lengths, the
/// resulting iterator will be silently truncated to the length of the shortest
/// iterable. This can lead to subtle bugs.
///
/// Pass `strict=True` to raise a `ValueError` if the iterables are of
/// non-uniform length. Alternatively, if the iterables are deliberately of
/// different lengths, pass `strict=False` to make the intention explicit.
///
/// ## Example
/// ```python
/// map(f, a, b)
/// ```
///
/// Use instead:
/// ```python
/// map(f, a, b, strict=True)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe for `map` calls that contain
/// `**kwargs`, as adding a `strict` keyword argument to such a call may lead
/// to a duplicate keyword argument error.
///
/// ## References
/// - [Python documentation: `map`](https://docs.python.org/3/library/functions.html#map)
/// - [Whats New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html)
#[derive(ViolationMetadata)]
pub(crate) struct MapWithoutExplicitStrict;
impl AlwaysFixableViolation for MapWithoutExplicitStrict {
#[derive_message_formats]
fn message(&self) -> String {
"`map()` without an explicit `strict=` parameter".to_string()
}
fn fix_title(&self) -> String {
"Add explicit value for parameter `strict=`".to_string()
}
}
/// B912
pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCall) {
let semantic = checker.semantic();
if semantic.match_builtin_expr(&call.func, "map")
&& call.arguments.find_keyword("strict").is_none()
&& call.arguments.args.len() >= 3 // function + at least 2 iterables
&& !any_infinite_iterables(call.arguments.args.iter().skip(1), semantic)
{
checker
.report_diagnostic(MapWithoutExplicitStrict, call.range())
.set_fix(Fix::applicable_edit(
add_argument(
"strict=False",
&call.arguments,
checker.comment_ranges(),
checker.locator().contents(),
),
// If the function call contains `**kwargs`, mark the fix as unsafe.
if call
.arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
Applicability::Unsafe
} else {
Applicability::Safe
},
));
}
}

View File

@@ -16,6 +16,7 @@ pub(crate) use getattr_with_constant::*;
pub(crate) use jump_statement_in_finally::*;
pub(crate) use loop_iterator_mutation::*;
pub(crate) use loop_variable_overrides_iterator::*;
pub(crate) use map_without_explicit_strict::*;
pub(crate) use mutable_argument_default::*;
pub(crate) use mutable_contextvar_default::*;
pub(crate) use no_explicit_stacklevel::*;
@@ -56,6 +57,7 @@ mod getattr_with_constant;
mod jump_statement_in_finally;
mod loop_iterator_mutation;
mod loop_variable_overrides_iterator;
mod map_without_explicit_strict;
mod mutable_argument_default;
mod mutable_contextvar_default;
mod no_explicit_stacklevel;

View File

@@ -90,7 +90,11 @@ pub(crate) fn unreliable_callable_check(
expr: &Expr,
func: &Expr,
args: &[Expr],
keywords: &[ast::Keyword],
) {
if !keywords.is_empty() {
return;
}
let [obj, attr, ..] = args else {
return;
};
@@ -103,7 +107,21 @@ pub(crate) fn unreliable_callable_check(
let Some(builtins_function) = checker.semantic().resolve_builtin_symbol(func) else {
return;
};
if !matches!(builtins_function, "hasattr" | "getattr") {
// Validate function arguments based on function name
let valid_args = match builtins_function {
"hasattr" => {
// hasattr should have exactly 2 positional arguments and no keywords
args.len() == 2
}
"getattr" => {
// getattr should have 2 or 3 positional arguments and no keywords
args.len() == 2 || args.len() == 3
}
_ => return,
};
if !valid_args {
return;
}

View File

@@ -1,11 +1,11 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_python_ast::{self as ast};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
@@ -57,11 +57,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
if semantic.match_builtin_expr(&call.func, "zip")
&& call.arguments.find_keyword("strict").is_none()
&& !call
.arguments
.args
.iter()
.any(|arg| is_infinite_iterable(arg, semantic))
&& !any_infinite_iterables(call.arguments.args.iter(), semantic)
{
checker
.report_diagnostic(ZipWithoutExplicitStrict, call.range())
@@ -86,47 +82,3 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
));
}
}
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
/// `itertools.cycle` or similar).
pub(crate) fn is_infinite_iterable(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, keywords, .. },
..
}) = &arg
else {
return false;
};
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
match qualified_name.segments() {
["itertools", "cycle" | "count"] => true,
["itertools", "repeat"] => {
// Ex) `itertools.repeat(1)`
if keywords.is_empty() && args.len() == 1 {
return true;
}
// Ex) `itertools.repeat(1, None)`
if args.len() == 2 && args[1].is_none_literal_expr() {
return true;
}
// Ex) `iterools.repeat(1, times=None)`
for keyword in keywords {
if keyword.arg.as_ref().is_some_and(|name| name == "times") {
if keyword.value.is_none_literal_expr() {
return true;
}
}
}
false
}
_ => false,
}
})
}

View File

@@ -156,4 +156,6 @@ help: Replace with `callable()`
- assert hasattr(A(), "__call__")
53 + assert callable(A())
54 | assert callable(A()) is False
55 |
56 | # https://github.com/astral-sh/ruff/issues/20440
note: This is an unsafe fix and may change runtime behavior

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---

View File

@@ -0,0 +1,141 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:5:1
|
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
|
help: Add explicit value for parameter `strict=`
2 |
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
5 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:6:1
|
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
|
help: Add explicit value for parameter `strict=`
3 | # Errors
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
- map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
6 + map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9], strict=False)
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:7:1
|
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
|
help: Add explicit value for parameter `strict=`
4 | map(lambda x: x, [1, 2, 3])
5 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
8 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
10 |
11 | # Errors (limited iterators).
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:9:1
|
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 |
11 | # Errors (limited iterators).
|
help: Add explicit value for parameter `strict=`
6 | map(lambda x, y, z: x + y + z, [1, 2, 3], [4, 5, 6], [7, 8, 9])
7 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]))
8 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9]), strict=False)
- map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
9 + map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True), strict=False)
10 |
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:12:1
|
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
|
help: Add explicit value for parameter `strict=`
9 | map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], *map(lambda x: x, [7, 8, 9], strict=True))
10 |
11 | # Errors (limited iterators).
- map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
12 + map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1), strict=False)
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
14 |
15 | import builtins
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:13:1
|
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14 |
15 | import builtins
|
help: Add explicit value for parameter `strict=`
10 |
11 | # Errors (limited iterators).
12 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, 1))
- map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
13 + map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4), strict=False)
14 |
15 | import builtins
16 | # Still an error even though it uses the qualified name
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:17:1
|
15 | import builtins
16 | # Still an error even though it uses the qualified name
17 | builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 |
19 | # OK
|
help: Add explicit value for parameter `strict=`
14 |
15 | import builtins
16 | # Still an error even though it uses the qualified name
- builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6])
17 + builtins.map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6], strict=False)
18 |
19 | # OK
20 | map(lambda x: x, [1, 2, 3], strict=True)

View File

@@ -14,6 +14,7 @@ mod tests {
use crate::registry::Rule;
use crate::rules::flake8_builtins;
use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_resource_path};
use ruff_python_ast::PythonVersion;
@@ -63,6 +64,28 @@ mod tests {
Ok(())
}
#[test_case(Rule::BuiltinAttributeShadowing, Path::new("A003.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_builtins").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
flake8_builtins: flake8_builtins::settings::Settings {
strict_checking: true,
..Default::default()
},
..LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(
Rule::StdlibModuleShadowing,
Path::new("A005/modules/utils/logging.py"),

View File

@@ -6,6 +6,7 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_a003_class_scope_shadowing_expansion_enabled;
use crate::rules::flake8_builtins::helpers::shadows_builtin;
/// ## What it does
@@ -123,16 +124,26 @@ pub(crate) fn builtin_attribute_shadowing(
// def repeat(value: int, times: int) -> list[int]:
// return [value] * times
// ```
// In stable, only consider references whose first non-type parent scope is the class
// scope (e.g., decorators, default args, and attribute initializers).
// In preview, also consider references from within the class scope.
let consider_reference = |reference_scope_id: ScopeId| {
if is_a003_class_scope_shadowing_expansion_enabled(checker.settings()) {
if reference_scope_id == scope_id {
return true;
}
}
checker
.semantic()
.first_non_type_parent_scope_id(reference_scope_id)
== Some(scope_id)
};
for reference in binding
.references
.iter()
.map(|reference_id| checker.semantic().reference(*reference_id))
.filter(|reference| {
checker
.semantic()
.first_non_type_parent_scope_id(reference.scope_id())
== Some(scope_id)
})
.filter(|reference| consider_reference(reference.scope_id()))
{
checker.report_diagnostic(
BuiltinAttributeShadowing {

View File

@@ -0,0 +1,50 @@
---
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
---
A003 Python builtin is shadowed by method `str` from line 14
--> A003.py:17:31
|
15 | pass
16 |
17 | def method_usage(self) -> str:
| ^^^
18 | pass
|
A003 Python builtin is shadowed by class attribute `id` from line 3
--> A003.py:20:34
|
18 | pass
19 |
20 | def attribute_usage(self) -> id:
| ^^
21 | pass
|
A003 Python builtin is shadowed by method `property` from line 26
--> A003.py:31:7
|
29 | id = 1
30 |
31 | @[property][0]
| ^^^^^^^^
32 | def f(self, x=[id]):
33 | return x
|
A003 Python builtin is shadowed by class attribute `id` from line 29
--> A003.py:32:20
|
31 | @[property][0]
32 | def f(self, x=[id]):
| ^^
33 | return x
|
A003 Python builtin is shadowed by class attribute `bin` from line 35
--> A003.py:36:12
|
35 | bin = 2
36 | foo = [bin]
| ^^^
|

View File

@@ -124,7 +124,7 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
let needs_trailing_comma = if let [item] = elts.as_slice() {
SimpleTokenizer::new(
checker.locator().contents(),
TextRange::new(item.end(), call.end()),
TextRange::new(item.end(), argument.end()),
)
.all(|token| token.kind != SimpleTokenKind::Comma)
} else {

View File

@@ -247,3 +247,36 @@ help: Rewrite as a tuple literal
28 | tuple([x for x in range(5)])
29 | tuple({x for x in range(10)})
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:46:6
|
44 | )
45 |
46 | t9 = tuple([1],)
| ^^^^^^^^^^^
47 | t10 = tuple([1, 2],)
|
help: Rewrite as a tuple literal
43 | }
44 | )
45 |
- t9 = tuple([1],)
46 + t9 = (1,)
47 | t10 = tuple([1, 2],)
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:47:7
|
46 | t9 = tuple([1],)
47 | t10 = tuple([1, 2],)
| ^^^^^^^^^^^^^^
|
help: Rewrite as a tuple literal
44 | )
45 |
46 | t9 = tuple([1],)
- t10 = tuple([1, 2],)
47 + t10 = (1, 2)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -344,3 +344,36 @@ help: Rewrite as a generator
42 | x for x in [1,2,3]
43 | }
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:46:6
|
44 | )
45 |
46 | t9 = tuple([1],)
| ^^^^^^^^^^^
47 | t10 = tuple([1, 2],)
|
help: Rewrite as a tuple literal
43 | }
44 | )
45 |
- t9 = tuple([1],)
46 + t9 = (1,)
47 | t10 = tuple([1, 2],)
note: This is an unsafe fix and may change runtime behavior
C409 [*] Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal)
--> C409.py:47:7
|
46 | t9 = tuple([1],)
47 | t10 = tuple([1, 2],)
| ^^^^^^^^^^^^^^
|
help: Rewrite as a tuple literal
44 | )
45 |
46 | t9 = tuple([1],)
- t10 = tuple([1, 2],)
47 + t10 = (1, 2)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,8 +1,9 @@
use ruff_python_ast::InterpolatedStringElement;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator, StringFlags};
use ruff_python_semantic::analyze::logging;
use ruff_python_stdlib::logging::LoggingLevel;
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_fix_f_string_logging_enabled;
@@ -198,7 +199,7 @@ fn check_log_record_attr_clash(checker: &Checker, extra: &Keyword) {
}
#[derive(Debug, Copy, Clone)]
enum LoggingCallType {
pub(crate) enum LoggingCallType {
/// Logging call with a level method, e.g., `logging.info`.
LevelCall(LoggingLevel),
/// Logging call with an integer level as an argument, e.g., `logger.log(level, ...)`.
@@ -215,39 +216,41 @@ impl LoggingCallType {
}
}
/// Check logging calls for violations.
pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
pub(crate) fn find_logging_call(
checker: &Checker,
call: &ast::ExprCall,
) -> Option<(LoggingCallType, TextRange)> {
// Determine the call type (e.g., `info` vs. `exception`) and the range of the attribute.
let (logging_call_type, range) = match call.func.as_ref() {
match call.func.as_ref() {
Expr::Attribute(ast::ExprAttribute { value: _, attr, .. }) => {
let Some(call_type) = LoggingCallType::from_attribute(attr.as_str()) else {
return;
};
let call_type = LoggingCallType::from_attribute(attr.as_str())?;
if !logging::is_logger_candidate(
&call.func,
checker.semantic(),
&checker.settings().logger_objects,
) {
return;
return None;
}
(call_type, attr.range())
Some((call_type, attr.range()))
}
Expr::Name(_) => {
let Some(qualified_name) = checker
let qualified_name = checker
.semantic()
.resolve_qualified_name(call.func.as_ref())
else {
return;
};
.resolve_qualified_name(call.func.as_ref())?;
let ["logging", attribute] = qualified_name.segments() else {
return;
return None;
};
let Some(call_type) = LoggingCallType::from_attribute(attribute) else {
return;
};
(call_type, call.func.range())
let call_type = LoggingCallType::from_attribute(attribute)?;
Some((call_type, call.func.range()))
}
_ => return,
_ => None,
}
}
/// Check logging calls for violations.
pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
let Some((logging_call_type, range)) = find_logging_call(checker, call) else {
return;
};
// G001, G002, G003, G004

View File

@@ -58,8 +58,8 @@ enum Method {
}
impl Method {
fn is_split(self) -> bool {
matches!(self, Method::Split)
fn is_rsplit(self) -> bool {
matches!(self, Method::RSplit)
}
}
@@ -201,65 +201,19 @@ fn split_default(
method: Method,
settings: &LinterSettings,
) -> Option<Expr> {
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
// > no empty strings at the start or end if the string has leading or trailing whitespace.
// > Consequently, splitting an empty string or a string consisting of just whitespace with
// > a None separator returns [].
// https://docs.python.org/3/library/stdtypes.html#str.split
let string_val = str_value.to_str();
match max_split.cmp(&0) {
Ordering::Greater => {
if !is_maxsplit_without_separator_fix_enabled(settings) {
return None;
}
Ordering::Greater if !is_maxsplit_without_separator_fix_enabled(settings) => None,
Ordering::Greater | Ordering::Equal => {
let Ok(max_split) = usize::try_from(max_split) else {
return None;
};
let list_items: Vec<&str> = if method.is_split() {
string_val
.trim_start_matches(py_unicode_is_whitespace)
.splitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect()
} else {
let mut items: Vec<&str> = string_val
.trim_end_matches(py_unicode_is_whitespace)
.rsplitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect();
items.reverse();
items
};
let list_items = split_whitespace_with_maxsplit(string_val, max_split, method);
Some(construct_replacement(
&list_items,
str_value.first_literal_flags(),
))
}
Ordering::Equal => {
// Behavior for maxsplit = 0 when sep is None:
// - If the string is empty or all whitespace, result is [].
// - Otherwise:
// - " x ".split(maxsplit=0) -> ['x ']
// - " x ".rsplit(maxsplit=0) -> [' x']
// - "".split(maxsplit=0) -> []
// - " ".split(maxsplit=0) -> []
let processed_str = if method.is_split() {
string_val.trim_start_matches(py_unicode_is_whitespace)
} else {
string_val.trim_end_matches(py_unicode_is_whitespace)
};
let list_items: &[_] = if processed_str.is_empty() {
&[]
} else {
&[processed_str]
};
Some(construct_replacement(
list_items,
str_value.first_literal_flags(),
))
}
Ordering::Less => {
let list_items: Vec<&str> = string_val
.split(py_unicode_is_whitespace)
@@ -367,3 +321,107 @@ const fn py_unicode_is_whitespace(ch: char) -> bool {
| '\u{3000}'
)
}
struct WhitespaceMaxSplitIterator<'a> {
remaining: &'a str,
max_split: usize,
splits: usize,
method: Method,
}
impl<'a> WhitespaceMaxSplitIterator<'a> {
fn new(s: &'a str, max_split: usize, method: Method) -> Self {
let remaining = match method {
Method::Split => s.trim_start_matches(py_unicode_is_whitespace),
Method::RSplit => s.trim_end_matches(py_unicode_is_whitespace),
};
Self {
remaining,
max_split,
splits: 0,
method,
}
}
}
impl<'a> Iterator for WhitespaceMaxSplitIterator<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
return None;
}
if self.splits >= self.max_split {
let result = self.remaining;
self.remaining = "";
return Some(result);
}
self.splits += 1;
match self.method {
Method::Split => match self.remaining.split_once(py_unicode_is_whitespace) {
Some((s, remaining)) => {
self.remaining = remaining.trim_start_matches(py_unicode_is_whitespace);
Some(s)
}
None => Some(std::mem::take(&mut self.remaining)),
},
Method::RSplit => match self.remaining.rsplit_once(py_unicode_is_whitespace) {
Some((remaining, s)) => {
self.remaining = remaining.trim_end_matches(py_unicode_is_whitespace);
Some(s)
}
None => Some(std::mem::take(&mut self.remaining)),
},
}
}
}
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
// > consecutive whitespace are regarded as a single separator, and the result will contain
// > no empty strings at the start or end if the string has leading or trailing whitespace.
// > Consequently, splitting an empty string or a string consisting of just whitespace with
// > a None separator returns [].
// https://docs.python.org/3/library/stdtypes.html#str.split
fn split_whitespace_with_maxsplit(s: &str, max_split: usize, method: Method) -> Vec<&str> {
let mut result: Vec<_> = WhitespaceMaxSplitIterator::new(s, max_split, method).collect();
if method.is_rsplit() {
result.reverse();
}
result
}
#[cfg(test)]
mod tests {
use super::{Method, split_whitespace_with_maxsplit};
use test_case::test_case;
#[test_case(" ", 1, &[])]
#[test_case("a b", 1, &["a", "b"])]
#[test_case("a b", 2, &["a", "b"])]
#[test_case(" a b c d ", 2, &["a", "b", "c d "])]
#[test_case(" a b c ", 1, &["a", "b c "])]
#[test_case(" x ", 0, &["x "])]
#[test_case(" ", 0, &[])]
#[test_case("a\u{3000}b", 1, &["a", "b"])]
fn test_split_whitespace_with_maxsplit(s: &str, max_split: usize, expected: &[&str]) {
let parts = split_whitespace_with_maxsplit(s, max_split, Method::Split);
assert_eq!(parts, expected);
}
#[test_case(" ", 1, &[])]
#[test_case("a b", 1, &["a", "b"])]
#[test_case("a b", 2, &["a", "b"])]
#[test_case(" a b c d ", 2, &[" a b", "c", "d"])]
#[test_case(" a b c ", 1, &[" a b", "c"])]
#[test_case(" x ", 0, &[" x"])]
#[test_case(" ", 0, &[])]
#[test_case("a\u{3000}b", 1, &["a", "b"])]
fn test_rsplit_whitespace_with_maxsplit(s: &str, max_split: usize, expected: &[&str]) {
let parts = split_whitespace_with_maxsplit(s, max_split, Method::RSplit);
assert_eq!(parts, expected);
}
}

View File

@@ -1372,6 +1372,7 @@ SIM905 Consider using a list literal instead of `str.split`
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
@@ -1382,5 +1383,16 @@ SIM905 Consider using a list literal instead of `str.rsplit`
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:173:1
|
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with list literal

View File

@@ -1420,6 +1420,7 @@ SIM905 [*] Consider using a list literal instead of `str.split`
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
@@ -1428,6 +1429,7 @@ help: Replace with list literal
- " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
171 + ["a", "b", "c d "] # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
SIM905 [*] Consider using a list literal instead of `str.rsplit`
--> SIM905.py:172:1
@@ -1436,6 +1438,7 @@ SIM905 [*] Consider using a list literal instead of `str.rsplit`
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
173 | "a b".split(maxsplit=1) # ["a", "b"]
|
help: Replace with list literal
169 |
@@ -1443,3 +1446,19 @@ help: Replace with list literal
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
- " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
172 + [" a b", "c", "d"] # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:173:1
|
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
173 | "a b".split(maxsplit=1) # ["a", "b"]
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with list literal
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
- "a b".split(maxsplit=1) # ["a", "b"]
173 + ["a", "b"] # ["a", "b"]

View File

@@ -920,6 +920,42 @@ mod tests {
flakes("__annotations__", &[]);
}
#[test]
fn module_warningregistry() {
// Using __warningregistry__ should not be considered undefined.
flakes("__warningregistry__", &[]);
}
#[test]
fn module_annotate_py314_available() {
// __annotate__ is available starting in Python 3.14.
let diagnostics = crate::test::test_snippet(
"__annotate__",
&crate::settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY314.into(),
..crate::settings::LinterSettings::for_rules(vec![
crate::codes::Rule::UndefinedName,
])
},
);
assert!(diagnostics.is_empty());
}
#[test]
fn module_annotate_pre_py314_undefined() {
// __annotate__ is not available before Python 3.14.
let diagnostics = crate::test::test_snippet(
"__annotate__",
&crate::settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY313.into(),
..crate::settings::LinterSettings::for_rules(vec![
crate::codes::Rule::UndefinedName,
])
},
);
assert_eq!(diagnostics.len(), 1);
}
#[test]
fn magic_globals_file() {
// Use of the C{__file__} magic global should not emit an undefined name

View File

@@ -139,11 +139,11 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
return;
};
if !((first_arg_id == "__class__"
|| (first_arg_id == parent_name.as_str()
// If the first argument matches the class name, check if it's a local variable
// that shadows the class name. If so, don't apply UP008.
&& !checker.semantic().current_scope().has(first_arg_id)))
// The `super(__class__, self)` and `super(ParentClass, self)` patterns are redundant in Python 3
// when the first argument refers to the implicit `__class__` cell or to the enclosing class.
// Avoid triggering if a local variable shadows either name.
if !(((first_arg_id == "__class__") || (first_arg_id == parent_name.as_str()))
&& !checker.semantic().current_scope().has(first_arg_id)
&& second_arg_id == parent_arg.name().as_str())
{
return;

View File

@@ -293,7 +293,19 @@ fn handle_non_finite_float_special_case(
return None;
};
let normalized = as_non_finite_float_string_literal(float_arg)?;
let replacement_text = format!(r#"{constructor_name}("{normalized}")"#);
let replacement_text = format!(
r#"{constructor_name}("{}")"#,
// `Decimal.from_float(float(" -nan")) == Decimal("nan")`
if normalized == "-nan" {
// Here we do not attempt to remove just the '-' character.
// It may have been encoded (e.g. as '\N{hyphen-minus}')
// in the original source slice, and the added complexity
// does not make sense for this edge case.
"nan"
} else {
normalized
}
);
Some(Edit::range_replacement(replacement_text, call.range()))
}

View File

@@ -363,7 +363,7 @@ help: Replace with `Decimal` constructor
21 | _ = Decimal.from_float(float("-Infinity"))
22 | _ = Decimal.from_float(float("nan"))
- _ = Decimal.from_float(float("-NaN"))
23 + _ = Decimal("-nan")
23 + _ = Decimal("nan")
24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 | _ = Decimal.from_float(float(" -inF\n \t"))
@@ -655,7 +655,7 @@ help: Replace with `Decimal` constructor
62 |
63 | # Cases with non-finite floats - should produce safe fixes
- _ = Decimal.from_float(float("-nan"))
64 + _ = Decimal("-nan")
64 + _ = Decimal("nan")
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
@@ -673,7 +673,7 @@ help: Replace with `Decimal` constructor
63 | # Cases with non-finite floats - should produce safe fixes
64 | _ = Decimal.from_float(float("-nan"))
- _ = Decimal.from_float(float("\x2dnan"))
65 + _ = Decimal("-nan")
65 + _ = Decimal("nan")
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
FURB164 [*] Verbose method `from_float` in `Decimal` construction
@@ -689,4 +689,4 @@ help: Replace with `Decimal` constructor
64 | _ = Decimal.from_float(float("-nan"))
65 | _ = Decimal.from_float(float("\x2dnan"))
- _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
66 + _ = Decimal("-nan")
66 + _ = Decimal("nan")

View File

@@ -112,6 +112,7 @@ mod tests {
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_warns.py"))]
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))]
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]

View File

@@ -0,0 +1,131 @@
use std::str::FromStr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_literal::cformat::{CFormatPart, CFormatString, CFormatType};
use ruff_python_literal::format::FormatConversion;
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::flake8_logging_format::rules::{LoggingCallType, find_logging_call};
/// ## What it does
/// Checks for eager string conversion of arguments to `logging` calls.
///
/// ## Why is this bad?
/// Arguments to `logging` calls will be formatted as strings automatically, so it
/// is unnecessary and less efficient to eagerly format the arguments before passing
/// them in.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`lint.logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`lint.logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
///
/// logging.basicConfig(format="%(message)s", level=logging.INFO)
///
/// user = "Maria"
///
/// logging.info("%s - Something happened", str(user))
/// ```
///
/// Use instead:
/// ```python
/// import logging
///
/// logging.basicConfig(format="%(message)s", level=logging.INFO)
///
/// user = "Maria"
///
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `lint.logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
#[derive(ViolationMetadata)]
pub(crate) struct LoggingEagerConversion {
pub(crate) format_conversion: FormatConversion,
}
impl Violation for LoggingEagerConversion {
#[derive_message_formats]
fn message(&self) -> String {
let LoggingEagerConversion { format_conversion } = self;
let (format_str, call_arg) = match format_conversion {
FormatConversion::Str => ("%s", "str()"),
FormatConversion::Repr => ("%r", "repr()"),
FormatConversion::Ascii => ("%a", "ascii()"),
FormatConversion::Bytes => ("%b", "bytes()"),
};
format!("Unnecessary `{call_arg}` conversion when formatting with `{format_str}`")
}
}
/// RUF065
pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) {
let Some((logging_call_type, _range)) = find_logging_call(checker, call) else {
return;
};
let msg_pos = match logging_call_type {
LoggingCallType::LevelCall(_) => 0,
LoggingCallType::LogCall => 1,
};
// Extract a format string from the logging statement msg argument
let Some(Expr::StringLiteral(string_literal)) =
call.arguments.find_argument_value("msg", msg_pos)
else {
return;
};
let Ok(format_string) = CFormatString::from_str(string_literal.value.to_str()) else {
return;
};
// Iterate over % placeholders in format string and zip with logging statement arguments
for (spec, arg) in format_string
.iter()
.filter_map(|(_, part)| {
if let CFormatPart::Spec(spec) = part {
Some(spec)
} else {
None
}
})
.zip(call.arguments.args.iter().skip(msg_pos + 1))
{
// Check if the argument is a call to eagerly format a value
if let Expr::Call(ast::ExprCall { func, .. }) = arg {
let CFormatType::String(format_conversion) = spec.format_type else {
continue;
};
// Check for use of %s with str() or %r with repr()
if checker.semantic().match_builtin_expr(func.as_ref(), "str")
&& matches!(format_conversion, FormatConversion::Str)
|| checker.semantic().match_builtin_expr(func.as_ref(), "repr")
&& matches!(format_conversion, FormatConversion::Repr)
{
checker
.report_diagnostic(LoggingEagerConversion { format_conversion }, arg.range());
}
}
}
}

View File

@@ -23,6 +23,7 @@ pub(crate) use invalid_index_type::*;
pub(crate) use invalid_pyproject_toml::*;
pub(crate) use invalid_rule_code::*;
pub(crate) use legacy_form_pytest_raises::*;
pub(crate) use logging_eager_conversion::*;
pub(crate) use map_int_version_parsing::*;
pub(crate) use missing_fstring_syntax::*;
pub(crate) use mutable_class_default::*;
@@ -86,6 +87,7 @@ mod invalid_index_type;
mod invalid_pyproject_toml;
mod invalid_rule_code;
mod legacy_form_pytest_raises;
mod logging_eager_conversion;
mod map_int_version_parsing;
mod missing_fstring_syntax;
mod mutable_class_default;

View File

@@ -0,0 +1,83 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
assertion_line: 124
---
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:4:26
|
3 | # %s + str()
4 | logging.info("Hello %s", str("World!"))
| ^^^^^^^^^^^^^
5 | logging.log(logging.INFO, "Hello %s", str("World!"))
|
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:5:39
|
3 | # %s + str()
4 | logging.info("Hello %s", str("World!"))
5 | logging.log(logging.INFO, "Hello %s", str("World!"))
| ^^^^^^^^^^^^^
6 |
7 | # %s + repr()
|
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
--> RUF065.py:16:26
|
15 | # %r + repr()
16 | logging.info("Hello %r", repr("World!"))
| ^^^^^^^^^^^^^^
17 | logging.log(logging.INFO, "Hello %r", repr("World!"))
|
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
--> RUF065.py:17:39
|
15 | # %r + repr()
16 | logging.info("Hello %r", repr("World!"))
17 | logging.log(logging.INFO, "Hello %r", repr("World!"))
| ^^^^^^^^^^^^^^
18 |
19 | from logging import info, log
|
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:22:18
|
21 | # %s + str()
22 | info("Hello %s", str("World!"))
| ^^^^^^^^^^^^^
23 | log(logging.INFO, "Hello %s", str("World!"))
|
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:23:31
|
21 | # %s + str()
22 | info("Hello %s", str("World!"))
23 | log(logging.INFO, "Hello %s", str("World!"))
| ^^^^^^^^^^^^^
24 |
25 | # %s + repr()
|
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
--> RUF065.py:34:18
|
33 | # %r + repr()
34 | info("Hello %r", repr("World!"))
| ^^^^^^^^^^^^^^
35 | log(logging.INFO, "Hello %r", repr("World!"))
|
RUF065 Unnecessary `repr()` conversion when formatting with `%r`
--> RUF065.py:35:31
|
33 | # %r + repr()
34 | info("Hello %r", repr("World!"))
35 | log(logging.INFO, "Hello %r", repr("World!"))
| ^^^^^^^^^^^^^^
36 |
37 | def str(s): return f"str = {s}"
|

View File

@@ -30,7 +30,7 @@ fn generate_inline_tests() -> Result<()> {
test_files += install_tests(&tests.err, "crates/ruff_python_parser/resources/inline/err")?;
if !test_files.is_empty() {
anyhow::bail!("{}", test_files);
anyhow::bail!("{test_files}");
}
Ok(())

View File

@@ -20,9 +20,15 @@ pub const MAGIC_GLOBALS: &[&str] = &[
"__annotations__",
"__builtins__",
"__cached__",
"__warningregistry__",
"__file__",
];
/// Magic globals that are only available starting in specific Python versions.
///
/// `__annotate__` was introduced in Python 3.14.
static PY314_PLUS_MAGIC_GLOBALS: &[&str] = &["__annotate__"];
static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[
"ArithmeticError",
"AssertionError",
@@ -216,6 +222,21 @@ pub fn python_builtins(minor_version: u8, is_notebook: bool) -> impl Iterator<It
.copied()
}
/// Return the list of magic globals for the given Python minor version.
pub fn python_magic_globals(minor_version: u8) -> impl Iterator<Item = &'static str> {
let py314_magic_globals = if minor_version >= 14 {
Some(PY314_PLUS_MAGIC_GLOBALS)
} else {
None
};
py314_magic_globals
.into_iter()
.flatten()
.chain(MAGIC_GLOBALS)
.copied()
}
/// Returns `true` if the given name is that of a Python builtin.
///
/// Intended to be kept in sync with [`python_builtins`].

View File

@@ -287,7 +287,7 @@ impl UvFormatCommand {
"The installed version of uv does not support `uv format`; upgrade to a newer version"
);
}
anyhow::bail!("Failed to format document: {}", stderr);
anyhow::bail!("Failed to format document: {stderr}");
}
let formatted = String::from_utf8(result.stdout)

View File

@@ -187,12 +187,9 @@ impl Index {
anyhow!("Failed to convert workspace URL to file path: {workspace_url}")
})?;
self.settings.remove(&workspace_path).ok_or_else(|| {
anyhow!(
"Tried to remove non-existent workspace URI {}",
workspace_url
)
})?;
self.settings
.remove(&workspace_path)
.ok_or_else(|| anyhow!("Tried to remove non-existent workspace URI {workspace_url}"))?;
// O(n) complexity, which isn't ideal... but this is an uncommon operation.
self.documents
@@ -330,7 +327,7 @@ impl Index {
};
let Some(_) = self.documents.remove(&url) else {
anyhow::bail!("tried to close document that didn't exist at {}", url)
anyhow::bail!("tried to close document that didn't exist at {url}")
};
Ok(())
}
@@ -351,7 +348,7 @@ impl Index {
anyhow::bail!("Tried to open unavailable document `{key}`");
};
let Some(controller) = self.documents.get_mut(&url) else {
anyhow::bail!("Document controller not available at `{}`", url);
anyhow::bail!("Document controller not available at `{url}`");
};
Ok(controller)
}

View File

@@ -19,7 +19,7 @@ There are multiple versions for the different wasm-pack targets. See [here](http
This example uses the wasm-pack web target and is known to work with Vite.
```ts
import init, { Workspace, type Diagnostic } from '@astral-sh/ruff-api';
import init, { Workspace, type Diagnostic } from '@astral-sh/ruff-wasm-web';
const exampleDocument = `print('hello'); print("world")`

View File

@@ -158,9 +158,7 @@ impl Configuration {
.expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier");
if !required_version.contains(&ruff_pkg_version) {
return Err(anyhow!(
"Required version `{}` does not match the running version `{}`",
required_version,
RUFF_PKG_VERSION
"Required version `{required_version}` does not match the running version `{RUFF_PKG_VERSION}`"
));
}
}

View File

@@ -523,10 +523,42 @@ impl<'config> WalkPythonFilesState<'config> {
let (files, error) = self.merged.into_inner().unwrap();
error?;
Ok((files, self.resolver.into_inner().unwrap()))
let deduplicated_files = deduplicate_files(files);
Ok((deduplicated_files, self.resolver.into_inner().unwrap()))
}
}
/// Deduplicate files by path, prioritizing `Root` files over `Nested` files.
///
/// When the same path appears both as a directly specified input (`Root`)
/// and via directory traversal (`Nested`), keep the `Root` entry and drop
/// the `Nested` entry.
///
/// Dropping the root entry means that the explicitly passed path may be
/// unintentionally ignored, since it is treated as nested and can be excluded
/// despite being requested.
///
/// Concretely, with `lint.exclude = ["foo.py"]` and `ruff check . foo.py`,
/// we must keep `Root(foo.py)` and drop `Nested(foo.py)` so `foo.py` is
/// linted as the user requested.
fn deduplicate_files(mut files: ResolvedFiles) -> ResolvedFiles {
// Sort by path; for identical paths, prefer Root over Nested; place errors after files
files.sort_by(|a, b| match (a, b) {
(Ok(a_file), Ok(b_file)) => a_file.cmp(b_file),
(Ok(_), Err(_)) => Ordering::Less,
(Err(_), Ok(_)) => Ordering::Greater,
(Err(_), Err(_)) => Ordering::Equal,
});
files.dedup_by(|a, b| match (a, b) {
(Ok(a_file), Ok(b_file)) => a_file.path() == b_file.path(),
_ => false,
});
files
}
struct PythonFilesVisitorBuilder<'s, 'config> {
state: &'s WalkPythonFilesState<'config>,
transformer: &'s (dyn ConfigurationTransformer + Sync),
@@ -682,7 +714,7 @@ impl Drop for PythonFilesVisitor<'_, '_> {
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
pub enum ResolvedFile {
/// File explicitly passed to the CLI
Root(PathBuf),
@@ -715,18 +747,6 @@ impl ResolvedFile {
}
}
impl PartialOrd for ResolvedFile {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ResolvedFile {
fn cmp(&self, other: &Self) -> Ordering {
self.path().cmp(other.path())
}
}
/// Return `true` if the Python file at [`Path`] is _not_ excluded.
pub fn python_file_at_path(
path: &Path,

View File

@@ -42,6 +42,13 @@ Used to determine if an active Conda environment is the base environment or not.
Used to detect an activated Conda environment location.
If both `VIRTUAL_ENV` and `CONDA_PREFIX` are present, `VIRTUAL_ENV` will be preferred.
### `PYTHONPATH`
Adds additional directories to ty's search paths.
The format is the same as the shells PATH:
one or more directory pathnames separated by os appropriate pathsep
(e.g. colons on Unix or semicolons on Windows).
### `RAYON_NUM_THREADS`
Specifies an upper limit for the number of threads ty uses when performing work in parallel.

View File

@@ -31,7 +31,13 @@ mypy_primer \
```
This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the
diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/ty_python_semantic/resources/primer/good.txt))\$"`.
diff for all projects we currently enable in CI, run
```sh
SELECTOR="$(paste -s -d'|' "crates/ty_python_semantic/resources/primer/good.txt" | sed -e 's@(@\\(@g' -e 's@)@\\)@g')"
```
and then use `--project-selector "$SELECTOR"`.
You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `ty_paths` configuration
option to work correctly.

177
crates/ty/docs/rules.md generated
View File

@@ -36,7 +36,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L113)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L114)
</small>
**What it does**
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L158)
</small>
**What it does**
@@ -88,7 +88,7 @@ f(int) # error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L184)
</small>
**What it does**
@@ -117,7 +117,7 @@ a = 1
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L209)
</small>
**What it does**
@@ -147,7 +147,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L235)
</small>
**What it does**
@@ -177,7 +177,7 @@ class B(A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L299)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L300)
</small>
**What it does**
@@ -202,7 +202,7 @@ class B(A, A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L320)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L321)
</small>
**What it does**
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L524)
</small>
**What it does**
@@ -334,7 +334,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L547)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L548)
</small>
**What it does**
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L352)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L353)
</small>
**What it does**
@@ -445,7 +445,7 @@ an atypical memory layout.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L592)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L593)
</small>
**What it does**
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L633)
</small>
**What it does**
@@ -496,7 +496,7 @@ a: int = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1666)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1688)
</small>
**What it does**
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L655)
</small>
**What it does**
@@ -562,7 +562,7 @@ asyncio.run(main())
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L684)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685)
</small>
**What it does**
@@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L735)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L736)
</small>
**What it does**
@@ -609,7 +609,7 @@ with 1:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L756)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L757)
</small>
**What it does**
@@ -636,7 +636,7 @@ a: str
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L779)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L780)
</small>
**What it does**
@@ -678,7 +678,7 @@ except ZeroDivisionError:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L815)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L816)
</small>
**What it does**
@@ -709,7 +709,7 @@ class C[U](Generic[T]): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L568)
</small>
**What it does**
@@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L841)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L842)
</small>
**What it does**
@@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L890)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L891)
</small>
**What it does**
@@ -803,7 +803,7 @@ class B(metaclass=f): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L497)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L498)
</small>
**What it does**
@@ -833,7 +833,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L918)
</small>
**What it does**
@@ -881,7 +881,7 @@ def foo(x: int) -> int: ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L961)
</small>
**What it does**
@@ -905,7 +905,7 @@ def f(a: int = ''): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L434)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L435)
</small>
**What it does**
@@ -937,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L980)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981)
</small>
Checks for `raise` statements that raise non-exceptions or use invalid
@@ -984,7 +984,7 @@ def g():
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L613)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L614)
</small>
**What it does**
@@ -1007,7 +1007,7 @@ def func() -> int:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1023)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1024)
</small>
**What it does**
@@ -1061,7 +1061,7 @@ TODO #14889
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L869)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L870)
</small>
**What it does**
@@ -1086,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1062)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063)
</small>
**What it does**
@@ -1114,7 +1114,7 @@ TYPE_CHECKING = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1086)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1087)
</small>
**What it does**
@@ -1142,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1138)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1139)
</small>
**What it does**
@@ -1174,7 +1174,7 @@ f(10) # Error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1110)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1111)
</small>
**What it does**
@@ -1206,7 +1206,7 @@ class C:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1166)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1167)
</small>
**What it does**
@@ -1239,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1195)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196)
</small>
**What it does**
@@ -1262,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1765)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1787)
</small>
**What it does**
@@ -1293,7 +1293,7 @@ alice["age"] # KeyError
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1214)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1215)
</small>
**What it does**
@@ -1320,7 +1320,7 @@ func("string") # error: [no-matching-overload]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1237)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1238)
</small>
**What it does**
@@ -1342,7 +1342,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1255)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1256)
</small>
**What it does**
@@ -1366,7 +1366,7 @@ for i in 34: # TypeError: 'int' object is not iterable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1306)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1307)
</small>
**What it does**
@@ -1386,6 +1386,31 @@ def f(x: int) -> int: ...
f(1, x=2) # Error raised here
```
## `positional-only-parameter-as-kwarg`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542)
</small>
**What it does**
Checks for keyword arguments in calls that match positional-only parameters of the callable.
**Why is this bad?**
Providing a positional-only parameter as a keyword argument will raise `TypeError` at runtime.
**Example**
```python
def f(x: int, /) -> int: ...
f(x=1) # Error raised here
```
## `raw-string-type-annotation`
<small>
@@ -1420,7 +1445,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1664)
</small>
**What it does**
@@ -1448,7 +1473,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1398)
</small>
**What it does**
@@ -1475,7 +1500,7 @@ class B(A): ... # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1442)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1443)
</small>
**What it does**
@@ -1500,7 +1525,7 @@ f("foo") # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1420)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1421)
</small>
**What it does**
@@ -1526,7 +1551,7 @@ def _(x: int):
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1464)
</small>
**What it does**
@@ -1570,7 +1595,7 @@ class A:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1520)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1521)
</small>
**What it does**
@@ -1595,7 +1620,7 @@ f(x=1, y=2) # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563)
</small>
**What it does**
@@ -1621,7 +1646,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1585)
</small>
**What it does**
@@ -1644,7 +1669,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1582)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1604)
</small>
**What it does**
@@ -1667,7 +1692,7 @@ print(x) # NameError: name 'x' is not defined
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1275)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1276)
</small>
**What it does**
@@ -1702,7 +1727,7 @@ b1 < b2 < b1 # exception raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1601)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623)
</small>
**What it does**
@@ -1728,7 +1753,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1645)
</small>
**What it does**
@@ -1751,7 +1776,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L462)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L463)
</small>
**What it does**
@@ -1790,7 +1815,7 @@ class SubProto(BaseProto, Protocol):
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L278)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L279)
</small>
**What it does**
@@ -1838,21 +1863,21 @@ Use instead:
a = 20 / 0 # type: ignore
```
## `possibly-unbound-attribute`
## `possibly-missing-attribute`
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1327)
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1328)
</small>
**What it does**
Checks for possibly unbound attributes.
Checks for possibly missing attributes.
**Why is this bad?**
Attempting to access an unbound attribute will raise an `AttributeError` at runtime.
Attempting to access a missing attribute will raise an `AttributeError` at runtime.
**Examples**
@@ -1864,23 +1889,23 @@ class A:
A.c # AttributeError: type object 'A' has no attribute 'c'
```
## `possibly-unbound-implicit-call`
## `possibly-missing-implicit-call`
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L131)
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L132)
</small>
**What it does**
Checks for implicit calls to possibly unbound methods.
Checks for implicit calls to possibly missing methods.
**Why is this bad?**
Expressions such as `x[y]` and `x * y` call methods
under the hood (`__getitem__` and `__mul__` respectively).
Calling an unbound method will raise an `AttributeError` at runtime.
Calling a missing method will raise an `AttributeError` at runtime.
**Examples**
@@ -1894,21 +1919,21 @@ class A:
A()[0] # TypeError: 'A' object is not subscriptable
```
## `possibly-unbound-import`
## `possibly-missing-import`
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1349)
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1350)
</small>
**What it does**
Checks for imports of symbols that may be unbound.
Checks for imports of symbols that may be missing.
**Why is this bad?**
Importing an unbound module or name will raise a `ModuleNotFoundError`
Importing a missing module or name will raise a `ModuleNotFoundError`
or `ImportError` at runtime.
**Examples**
@@ -1929,7 +1954,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1694)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1716)
</small>
**What it does**
@@ -1954,7 +1979,7 @@ cast(int, f()) # Redundant
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1502)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1503)
</small>
**What it does**
@@ -2005,7 +2030,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1715)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1737)
</small>
**What it does**
@@ -2059,7 +2084,7 @@ def g():
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L703)
</small>
**What it does**
@@ -2096,7 +2121,7 @@ class D(C): ... # error: [unsupported-base]
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L261)
</small>
**What it does**
@@ -2118,7 +2143,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1376)
</small>
**What it does**

View File

@@ -1875,3 +1875,131 @@ fn default_root_python_package_pyi() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn pythonpath_is_respected() -> anyhow::Result<()> {
let case = CliTest::with_files([
("baz-dir/baz.py", "it = 42"),
(
"src/foo.py",
r#"
import baz
print(f"{baz.it}")
"#,
),
])?;
assert_cmd_snapshot!(case.command(),
@r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> src/foo.py:2:8
|
2 | import baz
| ^^^
3 | print(f"{baz.it}")
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. <temp_dir>/src (first-party code)
info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
assert_cmd_snapshot!(case.command()
.env("PYTHONPATH", case.root().join("baz-dir")),
@r#"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
Ok(())
}
#[test]
fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> {
let case = CliTest::with_files([
("baz-dir/baz.py", "it = 42"),
("foo-dir/foo.py", "it = 42"),
(
"src/main.py",
r#"
import baz
import foo
print(f"{baz.it}")
print(f"{foo.it}")
"#,
),
])?;
assert_cmd_snapshot!(case.command(),
@r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> src/main.py:2:8
|
2 | import baz
| ^^^
3 | import foo
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. <temp_dir>/src (first-party code)
info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `foo`
--> src/main.py:3:8
|
2 | import baz
3 | import foo
| ^^^
4 |
5 | print(f"{baz.it}")
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. <temp_dir>/src (first-party code)
info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
let pythonpath =
std::env::join_paths([case.root().join("baz-dir"), case.root().join("foo-dir")])?;
assert_cmd_snapshot!(case.command()
.env("PYTHONPATH", pythonpath),
@r#"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
Ok(())
}

View File

@@ -1713,4 +1713,348 @@ import numpy as np
(Bar)
");
}
#[test]
fn conditional_imports_new_import() {
let test = CursorTest::builder()
.source("foo.py", "MAGIC = 1")
.source("bar.py", "MAGIC = 2")
.source("quux.py", "MAGIC = 3")
.source(
"main.py",
"\
if os.getenv(\"WHATEVER\"):
from foo import MAGIC
else:
from bar import MAGIC
(<CURSOR>)
",
)
.build();
assert_snapshot!(
test.import("quux", "MAGIC"), @r#"
import quux
if os.getenv("WHATEVER"):
from foo import MAGIC
else:
from bar import MAGIC
(quux.MAGIC)
"#);
assert_snapshot!(
test.import_from("quux", "MAGIC"), @r#"
import quux
if os.getenv("WHATEVER"):
from foo import MAGIC
else:
from bar import MAGIC
(quux.MAGIC)
"#);
}
// FIXME: This test (and the one below it) aren't
// quite right. Namely, because we aren't handling
// multiple binding sites correctly, we don't see the
// existing `MAGIC` symbol.
#[test]
fn conditional_imports_existing_import1() {
let test = CursorTest::builder()
.source("foo.py", "MAGIC = 1")
.source("bar.py", "MAGIC = 2")
.source("quux.py", "MAGIC = 3")
.source(
"main.py",
"\
if os.getenv(\"WHATEVER\"):
from foo import MAGIC
else:
from bar import MAGIC
(<CURSOR>)
",
)
.build();
assert_snapshot!(
test.import("foo", "MAGIC"), @r#"
import foo
if os.getenv("WHATEVER"):
from foo import MAGIC
else:
from bar import MAGIC
(foo.MAGIC)
"#);
assert_snapshot!(
test.import_from("foo", "MAGIC"), @r#"
import foo
if os.getenv("WHATEVER"):
from foo import MAGIC
else:
from bar import MAGIC
(foo.MAGIC)
"#);
}
#[test]
fn conditional_imports_existing_import2() {
let test = CursorTest::builder()
.source("foo.py", "MAGIC = 1")
.source("bar.py", "MAGIC = 2")
.source("quux.py", "MAGIC = 3")
.source(
"main.py",
"\
if os.getenv(\"WHATEVER\"):
from foo import MAGIC
else:
from bar import MAGIC
(<CURSOR>)
",
)
.build();
assert_snapshot!(
test.import("bar", "MAGIC"), @r#"
import bar
if os.getenv("WHATEVER"):
from foo import MAGIC
else:
from bar import MAGIC
(bar.MAGIC)
"#);
assert_snapshot!(
test.import_from("bar", "MAGIC"), @r#"
import bar
if os.getenv("WHATEVER"):
from foo import MAGIC
else:
from bar import MAGIC
(bar.MAGIC)
"#);
}
// FIXME: This test (and the one below it) aren't quite right. We
// don't recognize the multiple declaration sites for `fubar`.
//
// In this case, it's not totally clear what we should do. Since we
// are trying to import `MAGIC` from `foo`, we could add a `from
// foo import MAGIC` within the first `if` block. Or we could try
// and "infer" something about the code assuming that we know
// `MAGIC` is in both `foo` and `bar`.
#[test]
fn conditional_imports_existing_module1() {
let test = CursorTest::builder()
.source("foo.py", "MAGIC = 1")
.source("bar.py", "MAGIC = 2")
.source("quux.py", "MAGIC = 3")
.source(
"main.py",
"\
if os.getenv(\"WHATEVER\"):
import foo as fubar
else:
import bar as fubar
(<CURSOR>)
",
)
.build();
assert_snapshot!(
test.import("foo", "MAGIC"), @r#"
import foo
if os.getenv("WHATEVER"):
import foo as fubar
else:
import bar as fubar
(foo.MAGIC)
"#);
assert_snapshot!(
test.import_from("foo", "MAGIC"), @r#"
from foo import MAGIC
if os.getenv("WHATEVER"):
import foo as fubar
else:
import bar as fubar
(MAGIC)
"#);
}
#[test]
fn conditional_imports_existing_module2() {
let test = CursorTest::builder()
.source("foo.py", "MAGIC = 1")
.source("bar.py", "MAGIC = 2")
.source("quux.py", "MAGIC = 3")
.source(
"main.py",
"\
if os.getenv(\"WHATEVER\"):
import foo as fubar
else:
import bar as fubar
(<CURSOR>)
",
)
.build();
assert_snapshot!(
test.import("bar", "MAGIC"), @r#"
import bar
if os.getenv("WHATEVER"):
import foo as fubar
else:
import bar as fubar
(bar.MAGIC)
"#);
assert_snapshot!(
test.import_from("bar", "MAGIC"), @r#"
from bar import MAGIC
if os.getenv("WHATEVER"):
import foo as fubar
else:
import bar as fubar
(MAGIC)
"#);
}
#[test]
fn try_imports_new_import() {
let test = CursorTest::builder()
.source("foo.py", "MAGIC = 1")
.source("bar.py", "MAGIC = 2")
.source("quux.py", "MAGIC = 3")
.source(
"main.py",
"\
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(<CURSOR>)
",
)
.build();
assert_snapshot!(
test.import("quux", "MAGIC"), @r"
import quux
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(quux.MAGIC)
");
assert_snapshot!(
test.import_from("quux", "MAGIC"), @r"
import quux
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(quux.MAGIC)
");
}
// FIXME: This test (and the one below it) aren't
// quite right. Namely, because we aren't handling
// multiple binding sites correctly, we don't see the
// existing `MAGIC` symbol.
#[test]
fn try_imports_existing_import1() {
let test = CursorTest::builder()
.source("foo.py", "MAGIC = 1")
.source("bar.py", "MAGIC = 2")
.source("quux.py", "MAGIC = 3")
.source(
"main.py",
"\
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(<CURSOR>)
",
)
.build();
assert_snapshot!(
test.import("foo", "MAGIC"), @r"
import foo
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(foo.MAGIC)
");
assert_snapshot!(
test.import_from("foo", "MAGIC"), @r"
import foo
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(foo.MAGIC)
");
}
#[test]
fn try_imports_existing_import2() {
let test = CursorTest::builder()
.source("foo.py", "MAGIC = 1")
.source("bar.py", "MAGIC = 2")
.source("quux.py", "MAGIC = 3")
.source(
"main.py",
"\
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(<CURSOR>)
",
)
.build();
assert_snapshot!(
test.import("bar", "MAGIC"), @r"
import bar
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(bar.MAGIC)
");
assert_snapshot!(
test.import_from("bar", "MAGIC"), @r"
import bar
try:
from foo import MAGIC
except ImportError:
from bar import MAGIC
(bar.MAGIC)
");
}
}

View File

@@ -264,6 +264,15 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
}
source_order::walk_expr(self, expr);
}
Expr::Attribute(attribute) => {
if self.in_assignment {
if attribute.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr.range().end(), ty);
}
}
source_order::walk_expr(self, expr);
}
Expr::Call(call) => {
let argument_names =
inlay_hint_function_argument_details(self.db, &self.model, call)
@@ -436,6 +445,31 @@ mod tests {
");
}
#[test]
fn test_assign_attribute_of_instance() {
let test = inlay_hint_test(
"
class A:
def __init__(self, y):
self.x = 1
self.y = y
a = A(2)
a.y = 3
",
);
assert_snapshot!(test.inlay_hints(), @r"
class A:
def __init__(self, y):
self.x[: Literal[1]] = 1
self.y[: Unknown] = y
a[: A] = A([y=]2)
a.y[: Literal[3]] = 3
");
}
#[test]
fn test_disabled_variable_types() {
let test = inlay_hint_test("x = 1");

View File

@@ -22,6 +22,7 @@ ruff_python_formatter = { workspace = true, optional = true }
ruff_text_size = { workspace = true }
ty_combine = { workspace = true }
ty_python_semantic = { workspace = true, features = ["serde"] }
ty_static = { workspace = true }
ty_vendored = { workspace = true }
anyhow = { workspace = true }

View File

@@ -34,6 +34,7 @@ use ty_python_semantic::{
PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SearchPathValidationError,
SearchPaths, SitePackagesPaths, SysPrefixPathOrigin,
};
use ty_static::EnvVars;
#[derive(
Debug,
@@ -295,14 +296,51 @@ impl Options {
roots
};
// collect the existing site packages
let mut extra_paths: Vec<SystemPathBuf> = environment
.extra_paths
.as_deref()
.unwrap_or_default()
.iter()
.map(|path| path.absolute(project_root, system))
.collect();
// read all the paths off the PYTHONPATH environment variable, check
// they exist as a directory, and add them to the vec of extra_paths
// as they should be checked before site-packages just like python
// interpreter does
if let Ok(python_path) = system.env_var(EnvVars::PYTHONPATH) {
for path in std::env::split_paths(python_path.as_str()) {
let path = match SystemPathBuf::from_path_buf(path) {
Ok(path) => path,
Err(path) => {
tracing::debug!(
"Skipping `{path}` listed in `PYTHONPATH` because the path is not valid UTF-8",
path = path.display()
);
continue;
}
};
let abspath = SystemPath::absolute(path, system.current_directory());
if !system.is_directory(&abspath) {
tracing::debug!(
"Skipping `{abspath}` listed in `PYTHONPATH` because the path doesn't exist or isn't a directory"
);
continue;
}
tracing::debug!(
"Adding `{abspath}` from the `PYTHONPATH` environment variable to `extra_paths`"
);
extra_paths.push(abspath);
}
}
let settings = SearchPathSettings {
extra_paths: environment
.extra_paths
.as_deref()
.unwrap_or_default()
.iter()
.map(|path| path.absolute(project_root, system))
.collect(),
extra_paths,
src_roots,
custom_typeshed: environment
.typeshed

View File

@@ -131,12 +131,12 @@ m: IntList = [1, 2, 3]
reveal_type(m) # revealed: list[int]
# TODO: this should type-check and avoid literal promotion
# error: [invalid-assignment] "Object of type `list[int]` is not assignable to `list[Literal[1, 2, 3]]`"
# error: [invalid-assignment] "Object of type `list[Unknown | int]` is not assignable to `list[Literal[1, 2, 3]]`"
n: list[typing.Literal[1, 2, 3]] = [1, 2, 3]
reveal_type(n) # revealed: list[Literal[1, 2, 3]]
# TODO: this should type-check and avoid literal promotion
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `list[LiteralString]`"
# error: [invalid-assignment] "Object of type `list[Unknown | str]` is not assignable to `list[LiteralString]`"
o: list[typing.LiteralString] = ["a", "b", "c"]
reveal_type(o) # revealed: list[LiteralString]
```
@@ -144,10 +144,10 @@ reveal_type(o) # revealed: list[LiteralString]
## Incorrect collection literal assignments are complained aobut
```py
# error: [invalid-assignment] "Object of type `list[int]` is not assignable to `list[str]`"
# error: [invalid-assignment] "Object of type `list[Unknown | int]` is not assignable to `list[str]`"
a: list[str] = [1, 2, 3]
# error: [invalid-assignment] "Object of type `set[int | str]` is not assignable to `set[int]`"
# error: [invalid-assignment] "Object of type `set[Unknown | int | str]` is not assignable to `set[int]`"
b: set[int] = {1, 2, "3"}
```
@@ -234,3 +234,47 @@ reveal_type(x) # revealed: Foo
x: int = 1
reveal_type(x) # revealed: Literal[1]
```
## Annotations influence generic call inference
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Literal
def f[T](x: T) -> list[T]:
return [x]
a = f("a")
reveal_type(a) # revealed: list[Literal["a"]]
b: list[int | Literal["a"]] = f("a")
reveal_type(b) # revealed: list[int | Literal["a"]]
c: list[int | str] = f("a")
reveal_type(c) # revealed: list[int | str]
d: list[int | tuple[int, int]] = f((1, 2))
reveal_type(d) # revealed: list[int | tuple[int, int]]
e: list[int] = f(True)
reveal_type(e) # revealed: list[int]
# error: [invalid-assignment] "Object of type `list[Literal["a"]]` is not assignable to `list[int]`"
g: list[int] = f("a")
# error: [invalid-assignment] "Object of type `list[Literal["a"]]` is not assignable to `tuple[int]`"
h: tuple[int] = f("a")
def f2[T: int](x: T) -> T:
return x
i: int = f2(True)
reveal_type(i) # revealed: int
j: int | str = f2(True)
reveal_type(j) # revealed: Literal[True]
```

View File

@@ -914,7 +914,7 @@ def _(flag: bool):
reveal_type(C3.attr2) # revealed: Literal["metaclass value", "class value"]
```
If the *metaclass* attribute is only partially defined, we emit a `possibly-unbound-attribute`
If the *metaclass* attribute is only partially defined, we emit a `possibly-missing-attribute`
diagnostic:
```py
@@ -924,12 +924,12 @@ def _(flag: bool):
attr1: str = "metaclass value"
class C4(metaclass=Meta4): ...
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(C4.attr1) # revealed: str
```
Finally, if both the metaclass attribute and the class-level attribute are only partially defined,
we union them and emit a `possibly-unbound-attribute` diagnostic:
we union them and emit a `possibly-missing-attribute` diagnostic:
```py
def _(flag1: bool, flag2: bool):
@@ -941,7 +941,7 @@ def _(flag1: bool, flag2: bool):
if flag2:
attr1 = "class value"
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(C5.attr1) # revealed: Unknown | Literal["metaclass value", "class value"]
```
@@ -1180,13 +1180,13 @@ def _(flag1: bool, flag2: bool):
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-unbound-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` may be missing"
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>`"
C.x = 100
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `x` on type `C1 | C2 | C3` may be missing"
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`"
@@ -1212,18 +1212,18 @@ def _(flag: bool, flag1: bool, flag2: bool):
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-unbound-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` may be missing"
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
C.x = 100
# Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually,
# Note: we might want to consider ignoring possibly-missing diagnostics for instance attributes eventually,
# see the "Possibly unbound/undeclared instance attribute" section below.
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `x` on type `C1 | C2 | C3` may be missing"
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
C().x = 100
```
@@ -1287,16 +1287,16 @@ def _(flag: bool):
if flag:
x = 2
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
Bar.x = 3
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
Bar().x = 3
```
@@ -1304,7 +1304,7 @@ def _(flag: bool):
We currently treat implicit instance attributes to be bound, even if they are only conditionally
defined within a method. If the class-level definition or the whole method is only conditionally
available, we emit a `possibly-unbound-attribute` diagnostic.
available, we emit a `possibly-missing-attribute` diagnostic.
#### Possibly unbound and undeclared
@@ -1484,17 +1484,17 @@ def _(flag: bool):
class B1: ...
def inner1(a_and_b: Intersection[A1, B1]):
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(a_and_b.x) # revealed: P
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
a_and_b.x = R()
# Same for class objects
def inner1_class(a_and_b: Intersection[type[A1], type[B1]]):
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(a_and_b.x) # revealed: P
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
a_and_b.x = R()
class A2:
@@ -1509,7 +1509,7 @@ def _(flag: bool):
# TODO: this should not be an error, we need better intersection
# handling in `validate_attribute_assignment` for this
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
a_and_b.x = R()
# Same for class objects
def inner2_class(a_and_b: Intersection[type[A2], type[B1]]):
@@ -1524,17 +1524,17 @@ def _(flag: bool):
x: Q = Q()
def inner3(a_and_b: Intersection[A3, B3]):
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(a_and_b.x) # revealed: P & Q
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
a_and_b.x = R()
# Same for class objects
def inner3_class(a_and_b: Intersection[type[A3], type[B3]]):
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(a_and_b.x) # revealed: P & Q
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
a_and_b.x = R()
class A4: ...
@@ -1649,7 +1649,7 @@ If an attribute is defined on the class, it takes precedence over the `__getattr
reveal_type(c.class_attr) # revealed: int
```
If the class attribute is possibly unbound, we union the type of the attribute with the fallback
If the class attribute is possibly missing, we union the type of the attribute with the fallback
type of the `__getattr__` method:
```py

View File

@@ -26,7 +26,7 @@ In particular, we should raise errors in the "possibly-undeclared-and-unbound" a
| **Diagnostic** | declared | possibly-undeclared | undeclared |
| ---------------- | -------- | ------------------------- | ------------------- |
| bound | | | |
| possibly-unbound | | `possibly-unbound-import` | ? |
| possibly-unbound | | `possibly-missing-import` | ? |
| unbound | | ? | `unresolved-import` |
## Declared
@@ -158,7 +158,7 @@ a = None
If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and
inferred types. This case is interesting because the "possibly declared" definition might not be the
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import`
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-missing-import`
error for both `a` and `b`:
`mod.py`:
@@ -177,8 +177,8 @@ else:
```
```py
# error: [possibly-unbound-import]
# error: [possibly-unbound-import]
# error: [possibly-missing-import] "Member `a` of module `mod` may be missing"
# error: [possibly-missing-import] "Member `b` of module `mod` may be missing"
from mod import a, b
reveal_type(a) # revealed: Literal[1] | Any
@@ -332,8 +332,8 @@ if flag():
```
```py
# error: [possibly-unbound-import]
# error: [possibly-unbound-import]
# error: [possibly-missing-import]
# error: [possibly-missing-import]
from mod import MyInt, C
reveal_type(MyInt) # revealed: <class 'int'>

View File

@@ -19,7 +19,7 @@ b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
reveal_type(b) # revealed: Unknown
```
## Possibly unbound `__call__` method
## Possibly missing `__call__` method
```py
def _(flag: bool):
@@ -29,7 +29,7 @@ def _(flag: bool):
return 1
a = PossiblyNotCallable()
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly missing `__call__` method)"
reveal_type(result) # revealed: int
```
@@ -105,7 +105,7 @@ reveal_type(c()) # revealed: int
## Union over callables
### Possibly unbound `__call__`
### Possibly missing `__call__`
```py
def outer(cond1: bool):
@@ -122,6 +122,6 @@ def outer(cond1: bool):
else:
a = Other()
# error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)"
# error: [call-non-callable] "Object of type `Test` is not callable (possibly missing `__call__` method)"
a()
```

View File

@@ -158,15 +158,15 @@ def _(flag: bool) -> None:
def __new__(cls):
return object.__new__(cls)
# error: [possibly-unbound-implicit-call]
# error: [possibly-missing-implicit-call]
reveal_type(Foo()) # revealed: Foo
# error: [possibly-unbound-implicit-call]
# error: [possibly-missing-implicit-call]
# error: [too-many-positional-arguments]
reveal_type(Foo(1)) # revealed: Foo
```
#### Possibly unbound `__call__` on `__new__` callable
#### Possibly missing `__call__` on `__new__` callable
```py
def _(flag: bool) -> None:
@@ -178,11 +178,11 @@ def _(flag: bool) -> None:
class Foo:
__new__ = Callable()
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly missing `__call__` method)"
reveal_type(Foo(1)) # revealed: Foo
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly missing `__call__` method)"
reveal_type(Foo()) # revealed: Foo
```
@@ -294,11 +294,11 @@ def _(flag: bool) -> None:
class Foo:
__init__ = Callable()
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly missing `__call__` method)"
reveal_type(Foo(1)) # revealed: Foo
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly missing `__call__` method)"
reveal_type(Foo()) # revealed: Foo
```

View File

@@ -114,7 +114,11 @@ def _(flag: bool):
this_fails = ThisFails()
# error: [possibly-unbound-implicit-call]
# TODO: this would be a friendlier diagnostic if we propagated the error up the stack
# and transformed it into a `[not-subscriptable]` error with a subdiagnostic explaining
# that the cause of the error was a possibly missing `__getitem__` method
#
# error: [possibly-missing-implicit-call] "Method `__getitem__` of type `ThisFails` may be missing"
reveal_type(this_fails[0]) # revealed: Unknown | str
```
@@ -270,6 +274,11 @@ def _(flag: bool):
return str(key)
c = C()
# error: [possibly-unbound-implicit-call]
# TODO: this would be a friendlier diagnostic if we propagated the error up the stack
# and transformed it into a `[not-subscriptable]` error with a subdiagnostic explaining
# that the cause of the error was a possibly missing `__getitem__` method
#
# error: [possibly-missing-implicit-call] "Method `__getitem__` of type `C` may be missing"
reveal_type(c[0]) # revealed: str
```

View File

@@ -90,8 +90,7 @@ still continue to use the old convention, so it is supported by ty as well.
def f(__x: int): ...
f(1)
# error: [missing-argument]
# error: [unknown-argument]
# error: [positional-only-parameter-as-kwarg]
f(__x=1)
```
@@ -131,11 +130,9 @@ class C:
@staticmethod
def static_method(self, __x: int): ...
# error: [missing-argument]
# error: [unknown-argument]
# error: [positional-only-parameter-as-kwarg]
C().method(__x=1)
# error: [missing-argument]
# error: [unknown-argument]
# error: [positional-only-parameter-as-kwarg]
C.class_method(__x="1")
C.static_method("x", __x=42) # fine
```
@@ -871,3 +868,296 @@ is_subtype_of(int, int, int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int, int)
```
## Keywords argument
A double-starred argument (`**kwargs`) can be used to pass an argument that implements the mapping
protocol. This is matched against any of the *unmatched* standard (positional or keyword),
keyword-only, and keywords (`**kwargs`) parameters.
### Empty
```py
def empty() -> None: ...
def _(kwargs: dict[str, int]) -> None:
empty(**kwargs)
empty(**{})
empty(**dict())
```
### Single parameter
```py
from typing_extensions import TypedDict
def f(**kwargs: int) -> None: ...
class Foo(TypedDict):
a: int
b: int
def _(kwargs: dict[str, int]) -> None:
f(**kwargs)
f(**{"foo": 1})
f(**dict(foo=1))
f(**Foo(a=1, b=2))
```
### Positional-only and variadic parameters
```py
def f1(a: int, b: int, /) -> None: ...
def f2(*args: int) -> None: ...
def _(kwargs: dict[str, int]) -> None:
# error: [missing-argument] "No arguments provided for required parameters `a`, `b` of function `f1`"
f1(**kwargs)
# This doesn't raise an error because `*args` is an optional parameter and `**kwargs` can be empty.
f2(**kwargs)
```
### Standard parameters
```py
from typing_extensions import TypedDict
class Foo(TypedDict):
a: int
b: int
def f(a: int, b: int) -> None: ...
def _(kwargs: dict[str, int]) -> None:
f(**kwargs)
f(**{"a": 1, "b": 2})
f(**dict(a=1, b=2))
f(**Foo(a=1, b=2))
```
### Keyword-only parameters
```py
from typing_extensions import TypedDict
class Foo(TypedDict):
a: int
b: int
def f(*, a: int, b: int) -> None: ...
def _(kwargs: dict[str, int]) -> None:
f(**kwargs)
f(**{"a": 1, "b": 2})
f(**dict(a=1, b=2))
f(**Foo(a=1, b=2))
```
### Multiple keywords argument
```py
def f(**kwargs: int) -> None: ...
def _(kwargs1: dict[str, int], kwargs2: dict[str, int], kwargs3: dict[str, str], kwargs4: dict[int, list]) -> None:
f(**kwargs1, **kwargs2)
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
f(**kwargs1, **kwargs3)
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `list[Unknown]`"
f(**kwargs3, **kwargs4)
```
### Keyword-only after keywords
```py
class B: ...
def f(*, a: int, b: B, **kwargs: int) -> None: ...
def _(kwargs: dict[str, int]):
# Make sure that the `b` argument is not being matched against `kwargs` by passing an integer
# instead of the annotated type which should raise an
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `B`, found `Literal[2]`"
f(a=1, **kwargs, b=2)
```
### Mixed parameter kind
```py
def f1(*, a: int, b: int, **kwargs: int) -> None: ...
def f2(a: int, *, b: int, **kwargs: int) -> None: ...
def f3(a: int, /, *args: int, b: int, **kwargs: int) -> None: ...
def _(kwargs1: dict[str, int], kwargs2: dict[str, str]):
f1(**kwargs1)
f2(**kwargs1)
f3(1, **kwargs1)
```
### TypedDict
```py
from typing_extensions import NotRequired, TypedDict
class Foo1(TypedDict):
a: int
b: str
class Foo2(TypedDict):
a: int
b: NotRequired[str]
def f(**kwargs: int) -> None: ...
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
f(**Foo1(a=1, b="b"))
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
f(**Foo2(a=1))
```
### Keys must be strings
The keys of the mapping passed to a double-starred argument must be strings.
```py
from collections.abc import Mapping
def f(**kwargs: int) -> None: ...
class DictSubclass(dict[int, int]): ...
class MappingSubclass(Mapping[int, int]): ...
class MappingProtocol:
def keys(self) -> list[int]:
return [1]
def __getitem__(self, key: int) -> int:
return 1
def _(kwargs: dict[int, int]) -> None:
# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
f(**kwargs)
# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
f(**DictSubclass())
# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
f(**MappingSubclass())
# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
f(**MappingProtocol())
```
The key can also be a custom type that inherits from `str`.
```py
class SubStr(str): ...
class SubInt(int): ...
def _(kwargs1: dict[SubStr, int], kwargs2: dict[SubInt, int]) -> None:
f(**kwargs1)
# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `SubInt`"
f(**kwargs2)
```
Or, it can be a type that is assignable to `str`.
```py
from typing import Any
from ty_extensions import Unknown
def _(kwargs1: dict[Any, int], kwargs2: dict[Unknown, int]) -> None:
f(**kwargs1)
f(**kwargs2)
```
### Invalid value type
```py
from collections.abc import Mapping
def f(**kwargs: str) -> None: ...
class DictSubclass(dict[str, int]): ...
class MappingSubclass(Mapping[str, int]): ...
class MappingProtocol:
def keys(self) -> list[str]:
return ["foo"]
def __getitem__(self, key: str) -> int:
return 1
def _(kwargs: dict[str, int]) -> None:
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
f(**kwargs)
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
f(**DictSubclass())
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
f(**MappingSubclass())
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
f(**MappingProtocol())
```
### `Unknown` type
```py
from ty_extensions import Unknown
def f(**kwargs: int) -> None: ...
def _(kwargs: Unknown):
f(**kwargs)
```
### Not a mapping
```py
def f(**kwargs: int) -> None: ...
class A: ...
class InvalidMapping:
def keys(self) -> A:
return A()
def __getitem__(self, key: str) -> int:
return 1
def _(kwargs: dict[str, int] | int):
# error: [invalid-argument-type] "Argument expression after ** must be a mapping type: Found `dict[str, int] | int`"
f(**kwargs)
# error: [invalid-argument-type] "Argument expression after ** must be a mapping type: Found `InvalidMapping`"
f(**InvalidMapping())
```
### Generic
For a generic keywords parameter, the type variable should be specialized to the value type of the
mapping.
```py
from typing import TypeVar
_T = TypeVar("_T")
def f(**kwargs: _T) -> _T:
return kwargs["a"]
def _(kwargs: dict[str, int]) -> None:
reveal_type(f(**kwargs)) # revealed: int
```
For a `TypedDict`, the type variable should be specialized to the union of all value types.
```py
from typing import TypeVar
from typing_extensions import TypedDict
_T = TypeVar("_T")
class Foo(TypedDict):
a: int
b: str
def f(**kwargs: _T) -> _T:
return kwargs["a"]
reveal_type(f(**Foo(a=1, b="b"))) # revealed: int | str
```

View File

@@ -325,7 +325,7 @@ class D(metaclass=Meta):
reveal_type(D.f(1)) # revealed: Literal["a"]
```
If the class method is possibly unbound, we union the return types:
If the class method is possibly missing, we union the return types:
```py
def flag() -> bool:

View File

@@ -219,7 +219,7 @@ def f(x: C | D):
s = super(A, x)
reveal_type(s) # revealed: <super: <class 'A'>, C> | <super: <class 'A'>, D>
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: <class 'A'>, C> | <super: <class 'A'>, D>` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `b` on type `<super: <class 'A'>, C> | <super: <class 'A'>, D>` may be missing"
s.b
def f(flag: bool):
@@ -259,7 +259,7 @@ def f(flag: bool):
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
reveal_type(s.y) # revealed: int | str
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: <class 'B'>, B> | <super: <class 'D'>, D>` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `a` on type `<super: <class 'B'>, B> | <super: <class 'D'>, D>` may be missing"
reveal_type(s.a) # revealed: str
```

View File

@@ -351,7 +351,7 @@ reveal_type(C4.meta_attribute) # revealed: Literal["value on metaclass"]
reveal_type(C4.meta_non_data_descriptor) # revealed: Literal["non-data"]
```
When a metaclass data descriptor is possibly unbound, we union the result type of its `__get__`
When a metaclass data descriptor is possibly missing, we union the result type of its `__get__`
method with an underlying class level attribute, if present:
```py
@@ -365,7 +365,7 @@ def _(flag: bool):
meta_data_descriptor1: Literal["value on class"] = "value on class"
reveal_type(C5.meta_data_descriptor1) # revealed: Literal["data", "value on class"]
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(C5.meta_data_descriptor2) # revealed: Literal["data"]
# TODO: We currently emit two diagnostics here, corresponding to the two states of `flag`. The diagnostics are not
@@ -375,11 +375,11 @@ def _(flag: bool):
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `meta_data_descriptor1` of type `Literal["value on class"]`"
C5.meta_data_descriptor1 = None
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
C5.meta_data_descriptor2 = 1
```
When a class-level attribute is possibly unbound, we union its (descriptor protocol) type with the
When a class-level attribute is possibly missing, we union its (descriptor protocol) type with the
metaclass attribute (unless it's a data descriptor, which always takes precedence):
```py
@@ -401,7 +401,7 @@ def _(flag: bool):
reveal_type(C6.attribute1) # revealed: Literal["data"]
reveal_type(C6.attribute2) # revealed: Literal["non-data", "value on class"]
reveal_type(C6.attribute3) # revealed: Literal["value on metaclass", "value on class"]
# error: [possibly-unbound-attribute]
# error: [possibly-missing-attribute]
reveal_type(C6.attribute4) # revealed: Literal["value on class"]
```
@@ -756,16 +756,16 @@ def _(flag: bool):
non_data: NonDataDescriptor = NonDataDescriptor()
data: DataDescriptor = DataDescriptor()
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `<class 'PossiblyUnbound'>` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `non_data` on type `<class 'PossiblyUnbound'>` may be missing"
reveal_type(PossiblyUnbound.non_data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `non_data` on type `PossiblyUnbound` may be missing"
reveal_type(PossiblyUnbound().non_data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `data` on type `<class 'PossiblyUnbound'>` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `data` on type `<class 'PossiblyUnbound'>` may be missing"
reveal_type(PossiblyUnbound.data) # revealed: int
# error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
# error: [possibly-missing-attribute] "Attribute `data` on type `PossiblyUnbound` may be missing"
reveal_type(PossiblyUnbound().data) # revealed: int
```

View File

@@ -69,7 +69,7 @@ instance = C()
instance.non_existent = 1 # error: [unresolved-attribute]
```
## Possibly-unbound attributes
## Possibly-missing attributes
When trying to set an attribute that is not defined in all branches, we emit errors:
@@ -79,10 +79,10 @@ def _(flag: bool) -> None:
if flag:
attr: int = 0
C.attr = 1 # error: [possibly-unbound-attribute]
C.attr = 1 # error: [possibly-missing-attribute]
instance = C()
instance.attr = 1 # error: [possibly-unbound-attribute]
instance.attr = 1 # error: [possibly-missing-attribute]
```
## Data descriptors

View File

@@ -23,7 +23,7 @@ async def main() -> None:
await MissingAwait() # error: [invalid-await]
```
## Custom type with possibly unbound `__await__`
## Custom type with possibly missing `__await__`
This diagnostic also points to the method definition if available.

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