Compare commits

...

25 Commits

Author SHA1 Message Date
Carl Meyer
fb11171a9b [ty] avoid standalone expressions for simple subscript targets 2025-10-09 15:57:54 -07:00
pieterh-oai
66885e4bce [flake8-logging-format] Avoid dropping implicitly concatenated pieces in the G004 fix (#20793)
## Summary

The original autofix for G004 was quietly dropping everything but the
f-string components of any implicit concatenation sequence; this
addresses that.

Side note: It looks like `f_strings` is a bit risky to use (since it
implicitly skips non-f-string parts); use iter and include implicitly
concatenated pieces. We should consider if it's worth having
(convenience vs. bit risky).

## Test Plan

```
cargo test -p ruff_linter
```

Backtest (run new testcases against previous implementation):
```
git checkout HEAD^ crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs
cargot test -p ruff_linter

```
2025-10-09 18:14:38 -04:00
Carl Meyer
8248193ed9 [ty] defer inference of legacy TypeVar bound/constraints/defaults (#20598)
## Summary

This allows us to handle self-referential bounds/constraints/defaults
without panicking.

Handles more cases from https://github.com/astral-sh/ty/issues/256

This also changes the way we infer the types of legacy TypeVars. Rather
than understanding a constructor call to `typing[_extension].TypeVar`
inside of any (arbitrarily nested) expression, and having to use a
special `assigned_to` field of the semantic index to try to best-effort
figure out what name the typevar was assigned to, we instead understand
the creation of a legacy `TypeVar` only in the supported syntactic
position (RHS of a simple un-annotated assignment with one target). In
any other position, we just infer it as creating an opaque instance of
`typing.TypeVar`. (This behavior matches all other type checkers.)

So we now special-case TypeVar creation in `TypeInferenceBuilder`, as a
special case of an assignment definition, rather than deeper inside call
binding. This does mean we re-implement slightly more of
argument-parsing, but in practice this is minimal and easy to handle
correctly.

This is easier to implement if we also make the RHS of a simple (no
unpacking) one-target assignment statement no longer a standalone
expression. Which is fine to do, because simple one-target assignments
don't need to infer the RHS more than once. This is a bonus performance
(0-3% across various projects) and significant memory-usage win, since
most assignment statements are simple one-target assignment statements,
meaning we now create many fewer standalone-expression salsa
ingredients.

This change does mean that inference of manually-constructed
`TypeAliasType` instances can no longer find its Definition in
`assigned_to`, which regresses go-to-definition for these aliases. In a
future PR, `TypeAliasType` will receive the same treatment that
`TypeVar` did in this PR (moving its special-case inference into
`TypeInferenceBuilder` and supporting it only in the correct syntactic
position, and lazily inferring its value type to support recursion),
which will also fix the go-to-definition regression. (I decided a
temporary edge-case regression is better in this case than doubling the
size of this PR.)

This PR also tightens up and fixes various aspects of the validation of
`TypeVar` creation, as seen in the tests.

We still (for now) treat all typevars as instances of `typing.TypeVar`,
even if they were created using `typing_extensions.TypeVar`. This means
we'll wrongly error on e.g. `T.__default__` on Python 3.11, even if `T`
is a `typing_extensions.TypeVar` instance at runtime. We share this
wrong behavior with both mypy and pyrefly. It will be easier to fix
after we pull in https://github.com/python/typeshed/pull/14840.

There are some issues that showed up here with typevar identity and
`MarkTypeVarsInferable`; the fix here (using the new `original` field
and `is_identical_to` methods on `BoundTypeVarInstance` and
`TypeVarInstance`) is a bit kludgy, but it can go away when we eliminate
`MarkTypeVarsInferable`.

## Test Plan

Added and updated mdtests.

### Conformance suite impact

The impact here is all positive:

* We now correctly error on a legacy TypeVar with exactly one constraint
type given.
* We now correctly error on a legacy TypeVar with both an upper bound
and constraints specified.

### Ecosystem impact

Basically none; in the setuptools case we just issue slightly different
errors on an invalid TypeVar definition, due to the modified validation
code.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-10-09 21:08:37 +00:00
Ibraheem Ahmed
b086ffe921 [ty] Type-context aware literal promotion (#20776)
## Summary

Avoid literal promotion when a literal type annotation is provided, e.g.,
```py
x: list[Literal[1]] = [1]
```

Resolves https://github.com/astral-sh/ty/issues/1198. This does not fix
issue https://github.com/astral-sh/ty/issues/1284, but it does make it
more relevant because after this change, it is possible to directly
instantiate a generic type with a literal specialization.
2025-10-09 16:53:53 -04:00
Dan Parizher
537ec5f012 [fastapi] Fix false positives for path parameters that FastAPI doesn't recognize (FAST003) (#20687)
## Summary

Fixes #20680

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-10-09 16:10:21 -04:00
Shunsuke Shibayama
db91ac7dce [ty] allow any string Literal type expression as a key when constructing a TypedDict (#20792) 2025-10-09 18:24:11 +00:00
David Peter
75f3c0e8e6 [ty] Respect dataclass_transform parameters for metaclass-based models (#20780)
## Summary

Respect parameters such as `frozen_default` for metaclass-based
`@dataclass_transformer` models.

Related to: https://github.com/astral-sh/ty/issues/1260

## Typing conformance changes

Those are all correct (new true positives)

## Test Plan

New Markdown tests
2025-10-09 13:24:20 +00:00
wangxiaolei
f0d0b57900 [ty] dataclass_transform: Support frozen_default and kw_only_default (#20761)
## Summary

- Add support for eq, kw_only, and frozen parameter overrides in
@dataclass_transform
- Previously only order parameter override was supported
- Update test documentation to reflect fixed behavior
- Resolves issue where kw_only_default and frozen_default could not be
overridden

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

## Test Plan

New Markdown tests

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-10-09 09:34:49 +02:00
Alex Waygood
b0c6217e0b [ty] Fix broken property tests for disjointness of intersections (#20775)
## Summary

Two stable property tests are currently failing on `main`, following
f054b8a55e
(of course, I only thought to run the property tests again around 30
minutes _after_ landing that PR...). The issue is quite subtle, and took
me an annoying amount of time to pin down: we're matching over `(self,
other)` in `Type::is_disjoint_from_impl`, but `other` here is shadowed
by the binding in the `match` branch, which means that the wrong key is
inserted into the cache of the `IsDisjointFrom` cycle detector:


f054b8a55e/crates/ty_python_semantic/src/types.rs (L2408-L2435)

This PR fixes that issue, and also adds a few `Debug` implementations to
our cycle detectors, so that issues like this are easier to debug in the
future.

I'm adding the `internal` label, as this fixes a bug that hasn't yet
appeared in any released version of ty, so it doesn't deserve its own
changelog entry.

## Test Plan

`QUICKCHECK_TESTS=1000000 cargo test --release -p ty_python_semantic --
--ignored types::property_tests::stable` now once again passes on `main`

I considered adding new mdtests as well, but the examples that the
property tests were throwing at me all seemed _quite_ obscure and
somewhat unlikely to occur in the real world. I don't think it's worth
it.
2025-10-08 22:28:56 +01:00
Alex Waygood
f054b8a55e [ty] Improve assignability/subtyping between two protocol types (#20368) 2025-10-08 18:37:30 +00:00
Alex Waygood
b9c84add07 [ty] Disambiguate classes that live in different modules but have the same fully qualified names (#20756)
## Summary

Even disambiguating classes using their fully qualified names is not
enough for some diagnostics. We've seen real-world examples in the
ecosystem (and https://github.com/astral-sh/ruff/pull/20368 introduces
some more!) where two types can be different, but can still have the
same fully qualified name. In these cases, our disambiguation machinery
needs to print the file path and line number of the class in order to
disambiguate classes with similar names in our diagnostics.

Helps with https://github.com/astral-sh/ty/issues/1306

## Test Plan

Mdtests
2025-10-08 18:27:40 +01:00
David Peter
150ea92d03 [ty] Add tests for instance attributes in class hierarchies (#20767)
## Summary

This adds a couple of new test cases related to
https://github.com/astral-sh/ty/issues/1067 and beyond that. For now,
they are just documenting the current (problematic) behavior. Since the
topic has some subtleties, I'd like to merge this prior to the actual
bugfix(es) in order to evaluate the changes in an easier way.
2025-10-08 17:46:47 +02:00
David Peter
697998f836 [ty] Do not re-export ide_support attributes from types (#20769)
## Summary

The `types` module currently re-exports a lot of functions and data
types from `types::ide_support`. One of these is called `Member`, a name
that is overloaded several times already. And I'd like to add one more
`Member` struct soon. Making the whole `ide_support` module public seems
cleaner to me, anyway.

## Test Plan

Pure refactoring.
2025-10-08 17:45:28 +02:00
Andrew Gallant
3771f1567c [ty] Add an evaluation for completions
This is still early days, but I hope the framework introduced here makes
it very easy to add new truth data. Truth data should be seen as a form
of regression test for non-ideal ranking of completion suggestions.

I think it would help to read `crates/ty_completion_eval/README.md`
first to get an idea of what you're reviewing.
2025-10-08 08:44:21 -04:00
David Peter
6b94e620fe [ty] Fix accidental Liskov violation in protocol tests (#20763)
## Summary

We have the following test in `protocols.md`:
```py
class HasX(Protocol):
    x: int

# […]

class Foo:
    x: int

# […]

class FooBool(Foo):
    x: bool

static_assert(not is_subtype_of(FooBool, HasX))
static_assert(not is_assignable_to(FooBool, HasX))
```

If `Foo` was indeed intended to be a base class of `FooBool`, then `x:
bool` should be reported as a Liskov violation. And then it's a matter
of definition whether or not these assertions should hold true or not
(should the incorrect override take precedence or not?). So it looks to
me like this is just an oversight, probably a copy-paste error from
another test right before it, where `FooSub` is indeed intended to be a
subclass of `Foo`.

I am fixing this because this test started to fail on a branch of mine
that changes how attribute lookup in inheritance chains works.
2025-10-08 14:04:37 +02:00
David Peter
db80febb6b [ty] Use 3.14 in the ty playground (#20760)
## Summary

Use 3.14 by default in the ty playground

## Test Plan

Opened the playground locally and made sure that the default
configuration uses 3.14.
2025-10-08 12:41:57 +02:00
Mark Z. Ding
f95eb90951 [ty] Truncate type display for long unions in some situations (#20730)
## Summary

Fixes [astral-sh/ty#1307](https://github.com/astral-sh/ty/issues/1307)

Unions with length <= 5 are unaffected to minimize test churn
Unions with length > 5 will only display the first 3 elements + "...
omitted x union elements"
Here "length" is defined as the number of elements after condensation to
literals

Edit: we no longer truncate in revel case. 
Before:

> info: Attempted to call union type `(def f1() -> int) | (def f2(name:
str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) ->
int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) |
(Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`

After:

> info: Attempted to call union type `(def f1() -> int) | (def f2(name:
str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union
elements`

The below comparisons are outdated, but left here as a reference.

Before:
```reveal_type(x)  # revealed: Literal[1, 2] | A | B | C | D | E | F | G```
```reveal_type(x) # revealed: Result1A | Result1B | Result2A | Result2B
| Result3 | Result4```
After:
```reveal_type(x)  # revealed: Literal[1, 2] | A | B | ... omitted 5 union elements```
```reveal_type(x) # revealed: Result1A | Result1B | Result2A | ...
omitted 3 union elements```

This formatting is consistent with
`crates/ty_python_semantic/src/types/call/bind.rs` line 2992

## Test Plan

Cosmetic only, covered and verified by changes in mdtest
2025-10-08 11:21:26 +01:00
David Peter
1f1542db51 [ty] Use 3.14 as the default version (#20759)
## Summary

Bump the latest supported Python version of ty to 3.14 and updates some
references from 3.13 to 3.14.

This also fixes a bug with `dataclasses.field` on 3.14 (which adds a new
keyword-only parameter to that function, breaking our previously naive
matching on the parameter structure of that function).

## Test Plan

A `ty check` on a file with template strings (without any further
configuration) doesn't raise errors anymore.
2025-10-08 11:38:47 +02:00
Takayuki Maeda
abbbe8f3af [ruff] Use DiagnosticTag for more pyupgrade rules (#20734) 2025-10-08 06:52:43 +02:00
Carl Meyer
5d3a35e071 [ty] fix implicit Self on generic class with typevar default (#20754)
## Summary

Typevar attributes (bound/constraints/default) can be either lazily
evaluated or eagerly evaluated. Currently they are lazily evaluated for
PEP 695 typevars, and eager for legacy and synthetic typevars.
https://github.com/astral-sh/ruff/pull/20598 will make them lazy also
for legacy typevars, and the ecosystem report on that PR surfaced the
issue fixed here (because legacy typevars are much more common in the
ecosystem than PEP 695 typevars.)

Applying a transform to a typevar (normalization, materialization, or
mark-inferable) will reify all lazy attributes and create a new typevar
with eager attributes. In terms of Salsa identity, this transformed
typevar will be considered different from the original typevar, whether
or not the attributes were actually transformed.

In general, this is not a problem, since all typevars in a given generic
context will be transformed, or not, together.

The exception to this was implicit-self vs explicit Self annotations.
The typevar we created for implicit self was created initially using
inferable typevars, whereas an explicit Self annotation is initially
non-inferable, then transformed via mark-inferable when accessed as part
of a function signature. If the containing class (which becomes the
upper bound of `Self`) is generic, and has e.g. a lazily-evaluated
default, then the explicit-Self annotation will reify that default in
the upper bound, and the implicit-self would not, leading them to be
treated as different typevars, and causing us to fail to solve a call to
a method such as `def method(self) -> Self` correctly.

The fix here is to treat implicit-self more like explicit-Self,
initially creating it as non-inferable and then using the mark-inferable
transform on it. This is less efficient, but restores the invariant that
all typevars in a given generic context are transformed together, or
not, fixing the bug.

In the improved-constraint-solver work, the separation of typevars into
"inferable" and "non-inferable" is expected to disappear, along with the
mark-inferable transform, which would render both this bug and the fix
moot. So this fix is really just temporary until that lands.

There is a performance regression, but not a huge one: 1-2% on most
projects, 5% on one outlier. This seems acceptable, given that it should
be fully recovered by removing the mark-inferable transform.

## Test Plan

Added mdtests that failed before this change.
2025-10-08 01:38:24 +00:00
Alex Waygood
ff386b4797 [ty] Improve diagnostics for bad @overload definitions (#20745) 2025-10-07 21:52:57 +00:00
Dan Parizher
1bf4969c96 [ruff] Suppress diagnostic for f-string interpolations with debug text (RUF010) (#20525)
## Summary

Fixes #20519
2025-10-07 16:57:59 -04:00
liam
2be73e9afb [flake8-bugbear] Mark B905 and B912 fixes as unsafe (#20695)
Resolves https://github.com/astral-sh/ruff/issues/20694

This PR updates the `zip_without_explicit_strict` and
`map_without_explicit_strict` rules so their fixes are always marked
unsafe, following Brent's guidance that adding `strict=False` can
silently preserve buggy behaviour when inputs differ. The fix safety
docs now spell out that reasoning, the applicability drops to `Unsafe`,
and the snapshots were refreshed so Ruff clearly warns users before
applying the edit.
2025-10-07 16:55:56 -04:00
Amethyst Reese
7a347c4370 [ruff] update the release process documentation (#20752) 2025-10-07 13:18:48 -07:00
renovate[bot]
70b23a4fd0 Update actions/cache action to v4.3.0 (#20709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 19:56:21 +01:00
154 changed files with 5413 additions and 1272 deletions

View File

@@ -707,6 +707,24 @@ jobs:
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
ty-completion-evaluation:
name: "ty completion evaluation"
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Run ty completion evaluation"
run: cargo run --release --package ty_completion_eval -- all --threshold 0.1 --tasks /tmp/completion-evaluation-tasks.csv
- name: "Ensure there are no changes"
run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv
python-package:
name: "python package"
runs-on: ubuntu-latest
@@ -749,7 +767,7 @@ jobs:
with:
node-version: 22
- name: "Cache pre-commit"
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
@@ -911,7 +929,7 @@ jobs:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.ref == 'refs/heads/main' ||
github.ref == 'refs/heads/main' ||
(needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
timeout-minutes: 20
steps:
@@ -946,7 +964,7 @@ jobs:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.ref == 'refs/heads/main' ||
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
timeout-minutes: 20
steps:

View File

@@ -16,7 +16,8 @@ exclude: |
crates/ruff_python_formatter/resources/.*|
crates/ruff_python_formatter/tests/snapshots/.*|
crates/ruff_python_resolver/resources/.*|
crates/ruff_python_resolver/tests/snapshots/.*
crates/ruff_python_resolver/tests/snapshots/.*|
crates/ty_completion_eval/truth/.*
)$
repos:

View File

@@ -321,10 +321,16 @@ them to [PyPI](https://pypi.org/project/ruff/).
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).
### Creating a new release
### Installing tools
1. Install `uv`: `curl -LsSf https://astral.sh/uv/install.sh | sh`
1. Install `npm`: `brew install npm` or similar
### Creating a new release
Commit each step of this process separately for easier review.
1. Run `./scripts/release.sh`; this command will:
- Generate a temporary virtual environment with `rooster`
@@ -337,6 +343,7 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
- Square brackets (eg, `[ruff]` project name) will be automatically escaped by `pre-commit`
Additionally, for minor releases:
@@ -376,13 +383,13 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
1. Verify the GitHub release:
1. The Changelog should match the content of `CHANGELOG.md`
1. Append the contributors from the `scripts/release.sh` script
1. The changelog should match the content of `CHANGELOG.md`
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
1. One can determine if an update is needed when
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
1. Run `uv run --only-dev --no-sync scripts/update_schemastore.py --proto <https|ssh>`
1. Once run successfully, you should follow the link in the output to create a PR.
1. If needed, update the [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) and

42
Cargo.lock generated
View File

@@ -818,6 +818,27 @@ dependencies = [
"typenum",
]
[[package]]
name = "csv"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
dependencies = [
"memchr",
]
[[package]]
name = "ctrlc"
version = "3.5.0"
@@ -4228,6 +4249,26 @@ dependencies = [
"ty_python_semantic",
]
[[package]]
name = "ty_completion_eval"
version = "0.0.0"
dependencies = [
"anyhow",
"bstr",
"clap",
"csv",
"regex",
"ruff_db",
"ruff_text_size",
"serde",
"tempfile",
"toml",
"ty_ide",
"ty_project",
"ty_python_semantic",
"walkdir",
]
[[package]]
name = "ty_ide"
version = "0.0.0"
@@ -4402,6 +4443,7 @@ dependencies = [
"colored 3.0.0",
"insta",
"memchr",
"path-slash",
"regex",
"ruff_db",
"ruff_index",

View File

@@ -43,6 +43,7 @@ ruff_workspace = { path = "crates/ruff_workspace" }
ty = { path = "crates/ty" }
ty_combine = { path = "crates/ty_combine" }
ty_completion_eval = { path = "crates/ty_completion_eval" }
ty_ide = { path = "crates/ty_ide" }
ty_project = { path = "crates/ty_project", default-features = false }
ty_python_semantic = { path = "crates/ty_python_semantic" }
@@ -69,6 +70,7 @@ camino = { version = "1.1.7" }
clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "4.0.0" }
csv = { version = "1.3.1" }
divan = { package = "codspeed-divan-compat", version = "3.0.2" }
codspeed-criterion-compat = { version = "3.0.2", default-features = false }
colored = { version = "3.0.0" }
@@ -203,7 +205,7 @@ wild = { version = "2" }
zip = { version = "0.6.6", default-features = false }
[workspace.metadata.cargo-shear]
ignored = ["getrandom", "ruff_options_metadata", "uuid", "get-size2"]
ignored = ["getrandom", "ruff_options_metadata", "uuid", "get-size2", "ty_completion_eval"]
[workspace.lints.rust]

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,
},
600,
630,
)
});

View File

@@ -227,3 +227,32 @@ async def read_thing(query: str):
@app.get("/things/{ thing_id : str }")
async def read_thing(query: str):
return {"query": query}
# https://github.com/astral-sh/ruff/issues/20680
# These should NOT trigger FAST003 because FastAPI doesn't recognize them as path parameters
# Non-ASCII characters in parameter name
@app.get("/f1/{用户身份}")
async def f1():
return locals()
# Space in parameter name
@app.get("/f2/{x: str}")
async def f2():
return locals()
# Non-ASCII converter
@app.get("/f3/{complex_number:}")
async def f3():
return locals()
# Mixed non-ASCII characters
@app.get("/f4/{用户_id}")
async def f4():
return locals()
# Space in parameter name with converter
@app.get("/f5/{param: int}")
async def f5():
return locals()

View File

@@ -0,0 +1,8 @@
import logging
variablename = "value"
log = logging.getLogger(__name__)
log.info(f"a" f"b {variablename}")
log.info("a " f"b {variablename}")
log.info("prefix " f"middle {variablename}" f" suffix")

View File

@@ -56,3 +56,11 @@ f"{str(object=3)}"
f"{str(x for x in [])}"
f"{str((x for x in []))}"
# Debug text cases - should not trigger RUF010
f"{str(1)=}"
f"{ascii(1)=}"
f"{repr(1)=}"
f"{str('hello')=}"
f"{ascii('hello')=}"
f"{repr('hello')=}"

View File

@@ -1,12 +1,12 @@
use std::iter::Peekable;
use std::ops::Range;
use std::str::CharIndices;
use std::sync::LazyLock;
use regex::{CaptureMatches, Regex};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprSubscript, Parameter, ParameterWithDefault};
use ruff_python_semantic::{BindingKind, Modules, ScopeKind, SemanticModel};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::{Ranged, TextSize};
use crate::Fix;
@@ -165,11 +165,6 @@ pub(crate) fn fastapi_unused_path_parameter(
// Check if any of the path parameters are not in the function signature.
for (path_param, range) in path_params {
// Ignore invalid identifiers (e.g., `user-id`, as opposed to `user_id`)
if !is_identifier(path_param) {
continue;
}
// If the path parameter is already in the function or the dependency signature,
// we don't need to do anything.
if named_args.contains(&path_param) {
@@ -461,15 +456,19 @@ fn parameter_alias<'a>(parameter: &'a Parameter, semantic: &SemanticModel) -> Op
/// the parameter name. For example, `/{x}` is a valid parameter, but `/{ x }` is treated literally.
#[derive(Debug)]
struct PathParamIterator<'a> {
input: &'a str,
chars: Peekable<CharIndices<'a>>,
inner: CaptureMatches<'a, 'a>,
}
impl<'a> PathParamIterator<'a> {
fn new(input: &'a str) -> Self {
PathParamIterator {
input,
chars: input.char_indices().peekable(),
/// Matches the Starlette pattern for path parameters with optional converters from
/// <https://github.com/Kludex/starlette/blob/e18637c68e36d112b1983bc0c8b663681e6a4c50/starlette/routing.py#L121>
static FASTAPI_PATH_PARAM_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::[a-zA-Z_][a-zA-Z0-9_]*)?\}").unwrap()
});
Self {
inner: FASTAPI_PATH_PARAM_REGEX.captures_iter(input),
}
}
}
@@ -478,19 +477,10 @@ impl<'a> Iterator for PathParamIterator<'a> {
type Item = (&'a str, Range<usize>);
fn next(&mut self) -> Option<Self::Item> {
while let Some((start, c)) = self.chars.next() {
if c == '{' {
if let Some((end, _)) = self.chars.by_ref().find(|&(_, ch)| ch == '}') {
let param_content = &self.input[start + 1..end];
// We ignore text after a colon, since those are path converters
// See also: https://fastapi.tiangolo.com/tutorial/path-params/?h=path#path-convertor
let param_name_end = param_content.find(':').unwrap_or(param_content.len());
let param_name = &param_content[..param_name_end];
return Some((param_name, start..end + 1));
}
}
}
None
self.inner
.next()
// Extract the first capture group (the path parameter), but return the range of the
// whole match (everything in braces and including the braces themselves).
.and_then(|capture| Some((capture.get(1)?.as_str(), capture.get(0)?.range())))
}
}

View File

@@ -34,9 +34,10 @@ use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ```
///
/// ## 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.
/// This rule's fix is marked as unsafe. While adding `strict=False` preserves
/// the runtime behavior, it can obscure situations where the iterables are of
/// unequal length. Ruff prefers to alert users so they can choose the intended
/// behavior themselves.
///
/// ## References
/// - [Python documentation: `map`](https://docs.python.org/3/library/functions.html#map)
@@ -73,17 +74,7 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
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
},
Applicability::Unsafe,
));
}
}

View File

@@ -31,9 +31,10 @@ use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe for `zip` calls that contain
/// `**kwargs`, as adding a `strict` keyword argument to such a call may lead
/// to a duplicate keyword argument error.
/// This rule's fix is marked as unsafe. While adding `strict=False` preserves
/// the runtime behavior, it can obscure situations where the iterables are of
/// unequal length. Ruff prefers to alert users so they can choose the intended
/// behavior themselves.
///
/// ## References
/// - [Python documentation: `zip`](https://docs.python.org/3/library/functions.html#zip)
@@ -68,17 +69,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
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
},
Applicability::Unsafe,
));
}
}

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
assertion_line: 156
---
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:4:1
@@ -19,6 +20,7 @@ help: Add explicit value for parameter `strict=`
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:5:1
@@ -39,6 +41,7 @@ help: Add explicit value for parameter `strict=`
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:6:1
@@ -59,6 +62,7 @@ help: Add explicit value for parameter `strict=`
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:7:1
@@ -79,6 +83,7 @@ help: Add explicit value for parameter `strict=`
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
10 |
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:7:16
@@ -99,6 +104,7 @@ help: Add explicit value for parameter `strict=`
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
10 |
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:8:5
@@ -118,6 +124,7 @@ help: Add explicit value for parameter `strict=`
9 | zip(zip("a", strict=True))
10 |
11 | # OK
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:9:1
@@ -138,6 +145,7 @@ help: Add explicit value for parameter `strict=`
10 |
11 | # OK
12 | zip(range(3), strict=True)
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:24:1
@@ -156,6 +164,7 @@ help: Add explicit value for parameter `strict=`
25 | zip([1, 2, 3], repeat(1, times=4))
26 |
27 | import builtins
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:25:1
@@ -176,6 +185,7 @@ help: Add explicit value for parameter `strict=`
26 |
27 | import builtins
28 | # Still an error even though it uses the qualified name
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:29:1
@@ -191,3 +201,4 @@ help: Add explicit value for parameter `strict=`
28 | # Still an error even though it uses the qualified name
- builtins.zip([1, 2, 3])
29 + builtins.zip([1, 2, 3], strict=False)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
assertion_line: 112
---
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:5:1
@@ -20,6 +21,7 @@ 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)
note: This is an unsafe fix and may change runtime behavior
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:6:1
@@ -40,6 +42,7 @@ help: Add explicit value for parameter `strict=`
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))
note: This is an unsafe fix and may change runtime behavior
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:7:1
@@ -61,6 +64,7 @@ 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).
note: This is an unsafe fix and may change runtime behavior
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:9:1
@@ -81,6 +85,7 @@ 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))
note: This is an unsafe fix and may change runtime behavior
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:12:1
@@ -99,6 +104,7 @@ help: Add explicit value for parameter `strict=`
13 | map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=4))
14 |
15 | import builtins
note: This is an unsafe fix and may change runtime behavior
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:13:1
@@ -119,6 +125,7 @@ help: Add explicit value for parameter `strict=`
14 |
15 | import builtins
16 | # Still an error even though it uses the qualified name
note: This is an unsafe fix and may change runtime behavior
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:17:1
@@ -139,3 +146,4 @@ help: Add explicit value for parameter `strict=`
18 |
19 | # OK
20 | map(lambda x: x, [1, 2, 3], strict=True)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -23,6 +23,7 @@ mod tests {
#[test_case(Path::new("G003.py"))]
#[test_case(Path::new("G004.py"))]
#[test_case(Path::new("G004_arg_order.py"))]
#[test_case(Path::new("G004_implicit_concat.py"))]
#[test_case(Path::new("G010.py"))]
#[test_case(Path::new("G101_1.py"))]
#[test_case(Path::new("G101_2.py"))]
@@ -52,6 +53,7 @@ mod tests {
#[test_case(Rule::LoggingFString, Path::new("G004.py"))]
#[test_case(Rule::LoggingFString, Path::new("G004_arg_order.py"))]
#[test_case(Rule::LoggingFString, Path::new("G004_implicit_concat.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -42,38 +42,52 @@ fn logging_f_string(
// Default to double quotes if we can't determine it.
let quote_str = f_string
.value
.f_strings()
.iter()
.map(|part| match part {
ast::FStringPart::Literal(literal) => literal.flags.quote_str(),
ast::FStringPart::FString(f) => f.flags.quote_str(),
})
.next()
.map(|f| f.flags.quote_str())
.unwrap_or("\"");
for f in f_string.value.f_strings() {
for element in &f.elements {
match element {
InterpolatedStringElement::Literal(lit) => {
// If the literal text contains a '%' placeholder, bail out: mixing
// f-string interpolation with '%' placeholders is ambiguous for our
// automatic conversion, so don't offer a fix for this case.
if lit.value.as_ref().contains('%') {
return;
}
format_string.push_str(lit.value.as_ref());
for part in &f_string.value {
match part {
ast::FStringPart::Literal(literal) => {
let literal_text = literal.as_str();
if literal_text.contains('%') {
return;
}
InterpolatedStringElement::Interpolation(interpolated) => {
if interpolated.format_spec.is_some()
|| !matches!(
interpolated.conversion,
ruff_python_ast::ConversionFlag::None
)
{
return;
}
match interpolated.expression.as_ref() {
Expr::Name(name) => {
format_string.push_str("%s");
args.push(name.id.as_str());
format_string.push_str(literal_text);
}
ast::FStringPart::FString(f) => {
for element in &f.elements {
match element {
InterpolatedStringElement::Literal(lit) => {
// If the literal text contains a '%' placeholder, bail out: mixing
// f-string interpolation with '%' placeholders is ambiguous for our
// automatic conversion, so don't offer a fix for this case.
if lit.value.as_ref().contains('%') {
return;
}
format_string.push_str(lit.value.as_ref());
}
InterpolatedStringElement::Interpolation(interpolated) => {
if interpolated.format_spec.is_some()
|| !matches!(
interpolated.conversion,
ruff_python_ast::ConversionFlag::None
)
{
return;
}
match interpolated.expression.as_ref() {
Expr::Name(name) => {
format_string.push_str("%s");
args.push(name.id.as_str());
}
_ => return,
}
}
_ => return,
}
}
}

View File

@@ -0,0 +1,35 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
assertion_line: 50
---
G004 Logging statement uses f-string
--> G004_implicit_concat.py:6:10
|
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
| ^^^^^^^^^^^^^^^^^^^^^^^^
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004_implicit_concat.py:7:10
|
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
| ^^^^^^^^^^^^^^^^^^^^^^^^
8 | log.info("prefix " f"middle {variablename}" f" suffix")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004_implicit_concat.py:8:10
|
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -0,0 +1,53 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
assertion_line: 71
---
G004 [*] Logging statement uses f-string
--> G004_implicit_concat.py:6:10
|
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
| ^^^^^^^^^^^^^^^^^^^^^^^^
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
|
help: Convert to lazy `%` formatting
3 | variablename = "value"
4 |
5 | log = logging.getLogger(__name__)
- log.info(f"a" f"b {variablename}")
6 + log.info("ab %s", variablename)
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
G004 [*] Logging statement uses f-string
--> G004_implicit_concat.py:7:10
|
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
| ^^^^^^^^^^^^^^^^^^^^^^^^
8 | log.info("prefix " f"middle {variablename}" f" suffix")
|
help: Convert to lazy `%` formatting
4 |
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
- log.info("a " f"b {variablename}")
7 + log.info("a b %s", variablename)
8 | log.info("prefix " f"middle {variablename}" f" suffix")
G004 [*] Logging statement uses f-string
--> G004_implicit_concat.py:8:10
|
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
8 | log.info("prefix " f"middle {variablename}" f" suffix")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
5 | log = logging.getLogger(__name__)
6 | log.info(f"a" f"b {variablename}")
7 | log.info("a " f"b {variablename}")
- log.info("prefix " f"middle {variablename}" f" suffix")
8 + log.info("prefix middle %s suffix", variablename)

View File

@@ -43,6 +43,7 @@ where
T: Ranged,
{
let mut diagnostic = checker.report_diagnostic(DeprecatedCElementTree, node.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
let contents = checker.locator().slice(node);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
contents.replacen("cElementTree", "ElementTree", 1),

View File

@@ -265,6 +265,7 @@ pub(crate) fn deprecated_mock_attribute(checker: &Checker, attribute: &ast::Expr
},
attribute.value.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
"mock".to_string(),
attribute.value.range(),
@@ -313,6 +314,7 @@ pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) {
},
name.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
if let Some(content) = content.as_ref() {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
content.clone(),
@@ -351,6 +353,7 @@ pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) {
},
stmt.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
if let Some(indent) = indentation(checker.source(), stmt) {
diagnostic.try_set_fix(|| {
format_import_from(stmt, indent, checker.locator(), checker.stylist())

View File

@@ -98,6 +98,7 @@ pub(crate) fn deprecated_unittest_alias(checker: &Checker, expr: &Expr) {
},
expr.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("self.{target}"),
expr.range(),

View File

@@ -68,6 +68,7 @@ pub(crate) fn replace_universal_newlines(checker: &Checker, call: &ast::ExprCall
};
let mut diagnostic = checker.report_diagnostic(ReplaceUniversalNewlines, arg.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
if call.arguments.find_keyword("text").is_some() {
diagnostic.try_set_fix(|| {

View File

@@ -57,6 +57,7 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"]))
{
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias, expr.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
"str",

View File

@@ -110,10 +110,7 @@ pub(crate) fn explicit_f_string_type_conversion(checker: &Checker, f_string: &as
return;
}
let mut diagnostic =
checker.report_diagnostic(ExplicitFStringTypeConversion, expression.range());
// Don't support fixing f-string with debug text.
// Don't report diagnostic for f-string with debug text.
if element
.as_interpolation()
.is_some_and(|interpolation| interpolation.debug_text.is_some())
@@ -121,6 +118,9 @@ pub(crate) fn explicit_f_string_type_conversion(checker: &Checker, f_string: &as
return;
}
let mut diagnostic =
checker.report_diagnostic(ExplicitFStringTypeConversion, expression.range());
diagnostic.try_set_fix(|| {
convert_call_to_conversion_flag(checker, conversion, f_string, index, arg)
});

View File

@@ -293,18 +293,6 @@ help: Replace with conversion flag
48 | f"{repr(1)=}"
49 |
RUF010 Use explicit conversion flag
--> RUF010.py:48:4
|
46 | f"{builtins.repr(1)}"
47 |
48 | f"{repr(1)=}"
| ^^^^^^^
49 |
50 | f"{repr(lambda: 1)}"
|
help: Replace with conversion flag
RUF010 [*] Use explicit conversion flag
--> RUF010.py:50:4
|
@@ -383,6 +371,7 @@ help: Replace with conversion flag
56 + f"{(x for x in [])!s}"
57 |
58 | f"{str((x for x in []))}"
59 |
RUF010 [*] Use explicit conversion flag
--> RUF010.py:58:4
@@ -391,6 +380,8 @@ RUF010 [*] Use explicit conversion flag
57 |
58 | f"{str((x for x in []))}"
| ^^^^^^^^^^^^^^^^^^^^
59 |
60 | # Debug text cases - should not trigger RUF010
|
help: Replace with conversion flag
55 |
@@ -398,3 +389,6 @@ help: Replace with conversion flag
57 |
- f"{str((x for x in []))}"
58 + f"{(x for x in [])!s}"
59 |
60 | # Debug text cases - should not trigger RUF010
61 | f"{str(1)=}"

View File

@@ -67,8 +67,8 @@ impl PythonVersion {
}
pub const fn latest_ty() -> Self {
// Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
Self::PY313
// Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
Self::PY314
}
pub const fn as_tuple(self) -> (u8, u8) {

2
crates/ty/docs/cli.md generated
View File

@@ -76,7 +76,7 @@ over all configuration files.</p>
<p>This is used to specialize the type of <code>sys.platform</code> and will affect the visibility of platform-specific functions and attributes. If the value is set to <code>all</code>, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.</p>
</dd><dt id="ty-check--python-version"><a href="#ty-check--python-version"><code>--python-version</code></a>, <code>--target-version</code> <i>version</i></dt><dd><p>Python version to assume when resolving types.</p>
<p>The Python version affects allowed syntax, type definitions of the standard library, and type definitions of first- and third-party modules that are conditional on the Python version.</p>
<p>If a version is not specified on the command line or in a configuration file, ty will try the following techniques in order of preference to determine a value: 1. Check for the <code>project.requires-python</code> setting in a <code>pyproject.toml</code> file and use the minimum version from the specified range 2. Check for an activated or configured Python environment and attempt to infer the Python version of that environment 3. Fall back to the latest stable Python version supported by ty (currently Python 3.13)</p>
<p>If a version is not specified on the command line or in a configuration file, ty will try the following techniques in order of preference to determine a value: 1. Check for the <code>project.requires-python</code> setting in a <code>pyproject.toml</code> file and use the minimum version from the specified range 2. Check for an activated or configured Python environment and attempt to infer the Python version of that environment 3. Fall back to the latest stable Python version supported by ty (see <code>ty check --help</code> output)</p>
<p>Possible values:</p>
<ul>
<li><code>3.7</code></li>

View File

@@ -133,9 +133,9 @@ For some language features, ty can also understand conditionals based on compari
with `sys.version_info`. These are commonly found in typeshed, for example,
to reflect the differing contents of the standard library across Python versions.
**Default value**: `"3.13"`
**Default value**: `"3.14"`
**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | <major>.<minor>`
**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | <major>.<minor>`
**Example usage** (`pyproject.toml`):

191
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#L114)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L115)
</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#L158)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L159)
</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#L184)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L185)
</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#L209)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L210)
</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#L235)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L236)
</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#L300)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L301)
</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#L321)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L322)
</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#L524)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L525)
</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#L548)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L549)
</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#L353)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L354)
</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#L593)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L594)
</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#L633)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L634)
</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#L1688)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1745)
</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#L655)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L656)
</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#L685)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L686)
</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#L736)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737)
</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#L757)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L758)
</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#L780)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L781)
</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#L816)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L817)
</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#L568)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L569)
</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#L842)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L843)
</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#L891)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L892)
</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#L498)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L499)
</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#L918)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L919)
</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#L961)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1018)
</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#L435)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L436)
</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#L981)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1038)
</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#L614)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L615)
</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#L1024)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1081)
</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#L870)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L871)
</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#L1063)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1120)
</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#L1087)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1144)
</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#L1139)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196)
</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#L1111)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1168)
</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#L1167)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1224)
</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#L1196)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1253)
</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#L1787)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1844)
</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#L1215)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1272)
</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#L1238)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1295)
</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#L1256)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1313)
</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#L1307)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1364)
</small>
**What it does**
@@ -1391,7 +1391,7 @@ f(1, x=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%20positional-only-parameter-as-kwarg) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1599)
</small>
**What it does**
@@ -1445,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#L1664)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1721)
</small>
**What it does**
@@ -1473,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#L1398)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1455)
</small>
**What it does**
@@ -1500,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#L1443)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1500)
</small>
**What it does**
@@ -1525,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#L1421)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1478)
</small>
**What it does**
@@ -1551,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#L1464)
[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 +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#L1521)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1578)
</small>
**What it does**
@@ -1620,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#L1563)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1620)
</small>
**What it does**
@@ -1646,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#L1585)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642)
</small>
**What it does**
@@ -1669,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#L1604)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1661)
</small>
**What it does**
@@ -1692,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#L1276)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1333)
</small>
**What it does**
@@ -1727,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#L1623)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1680)
</small>
**What it does**
@@ -1753,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#L1645)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1702)
</small>
**What it does**
@@ -1776,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#L463)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L464)
</small>
**What it does**
@@ -1815,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#L279)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L280)
</small>
**What it does**
@@ -1868,7 +1868,7 @@ a = 20 / 0 # type: ignore
<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-missing-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1328)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1385)
</small>
**What it does**
@@ -1894,7 +1894,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
<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-missing-implicit-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L132)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L133)
</small>
**What it does**
@@ -1924,7 +1924,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
<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-missing-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1350)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1407)
</small>
**What it does**
@@ -1954,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#L1716)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1773)
</small>
**What it does**
@@ -1979,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#L1503)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1560)
</small>
**What it does**
@@ -2030,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#L1737)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1794)
</small>
**What it does**
@@ -2084,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#L703)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L704)
</small>
**What it does**
@@ -2116,12 +2116,73 @@ class D(C): ... # error: [unsupported-base]
[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
## `useless-overload-body`
<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%20useless-overload-body) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L962)
</small>
**What it does**
Checks for various `@overload`-decorated functions that have non-stub bodies.
**Why is this bad?**
Functions decorated with `@overload` are ignored at runtime; they are overridden
by the implementation function that follows the series of overloads. While it is
not illegal to provide a body for an `@overload`-decorated function, it may indicate
a misunderstanding of how the `@overload` decorator works.
**Example**
```py
from typing import overload
@overload
def foo(x: int) -> int:
return x + 1 # will never be executed
@overload
def foo(x: str) -> str:
return "Oh no, got a string" # will never be executed
def foo(x: int | str) -> int | str:
raise Exception("unexpected type encountered")
```
Use instead:
```py
from typing import assert_never, overload
@overload
def foo(x: int) -> int: ...
@overload
def foo(x: str) -> str: ...
def foo(x: int | str) -> int | str:
if isinstance(x, int):
return x + 1
elif isinstance(x, str):
return "Oh no, got a string"
else:
assert_never(x)
```
**References**
- [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload)
## `division-by-zero`
<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#L261)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L262)
</small>
**What it does**
@@ -2143,7 +2204,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#L1376)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1433)
</small>
**What it does**

View File

@@ -85,7 +85,7 @@ pub(crate) struct CheckCommand {
/// and use the minimum version from the specified range
/// 2. Check for an activated or configured Python environment
/// and attempt to infer the Python version of that environment
/// 3. Fall back to the latest stable Python version supported by ty (currently Python 3.13)
/// 3. Fall back to the latest stable Python version supported by ty (see `ty check --help` output)
#[arg(long, value_name = "VERSION", alias = "target-version")]
pub(crate) python_version: Option<PythonVersion>,

View File

@@ -0,0 +1,32 @@
[package]
name = "ty_completion_eval"
version = "0.0.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff_db = { workspace = true, features = ["os"] }
ruff_text_size = { workspace = true }
ty_ide = { workspace = true }
ty_project = { workspace = true }
ty_python_semantic = { workspace = true }
anyhow = { workspace = true }
bstr = { workspace = true }
clap = { workspace = true, features = ["wrap_help", "string", "env"] }
csv = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
tempfile = { workspace = true }
toml = { workspace = true }
walkdir = { workspace = true }
[lints]
workspace = true

View File

@@ -0,0 +1,143 @@
This directory contains a framework for evaluating completion suggestions
returned by the ty LSP.
# Running an evaluation
To run a full evaluation, run the `ty_completion_eval` crate with the
`all` command from the root of this repository:
```console
cargo run --release --package ty_completion_eval -- all
```
The output should look like this:
```text
Finished `release` profile [optimized] target(s) in 0.09s
Running `target/release/ty_completion_eval all`
mean reciprocal rank: 0.20409790112917506
MRR exceeds threshold of 0.001
```
If you want to look at the results of each individual evaluation task,
you can ask the evaluation to write CSV data that contains the rank of
the expected answer in each completion request:
```console
cargo r -r -p ty_completion_eval -- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
```
To debug a _specific_ task and look at the actual results, use the `show-one`
command:
```console
cargo r -q -p ty_completion_eval show-one higher-level-symbols-preferred --index 1
```
(The `--index` flag is only needed if there are multiple `<CURSOR>` directives in the same file.)
Has output that should look like this:
```text
ZQZQZQ_SOMETHING_IMPORTANT (*, 1/31)
__annotations__
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__file__
__format__
__getattr__
__getattribute__
__getstate__
__hash__
__init__
__init_subclass__
__loader__
__module__
__name__
__ne__
__new__
__package__
__path__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__spec__
__str__
__subclasshook__
-----
found 31 completions
```
The expected answer is marked with a `*`. The higher the rank, the better. In this example, the
rank is perfect. Note that the expected answer may not always appear in the completion results!
(Which is considered the worst possible outcome by this evaluation framework.)
# Evaluation model
This evaluation is based on [mean reciprocal rank] (MRR). That is, it assumes
that for every evaluation task (i.e., a single completion request) there is
precisely one correct answer. The higher the correct answer appears in each
completion request, the better. The mean reciprocal rank is computed as the
average of `1/rank` across all evaluation tasks. The higher the mean reciprocal
rank, the better.
The evaluation starts by preparing its truth data, which is contained in the `./truth` directory.
Within `./truth` is a list of Python projects. Every project contains one or more `<CURSOR>`
directives. Each `<CURSOR>` directive corresponds to an instruction to initiate a completion
request at that position. For example:
```python
class Foo:
def frobnicate(self): pass
foo = Foo()
foo.frob<CURSOR: frobnicate>
```
The above example says that completions should be requested immediately after `foo.frob`
_and_ that the expected answer is `frobnicate`.
When testing auto-import, one should also include the module in the expected answer.
For example:
```python
RegexFl<CURSOR: re.RegexFlag>
```
Settings for completion requests can be configured via a `completion.toml` file within
each Python project directory.
When an evaluation is run, the truth data is copied to a temporary directory.
`uv sync` is then run within each directory to prepare it.
# Continuous Integration
At time of writing (2025-10-07), an evaluation is run in CI. CI will fail if the MRR is
below a set threshold. When this occurs, it means that the evaluation's results have likely
gotten worse in some measurable way. Ideally, the way to fix this would be to fix whatever
regression occurred in ranking. One can follow the steps above to run an evaluation and
emit the individual task results in CSV format. This difference between this CSV data and
whatever is committed at `./crates/ty_completion_eval/completion-evaluation-tasks.csv` should
point to where the regression occurs.
If the change is not a regression or is otherwise expected, then the MRR threshold can be
lowered. This requires changing how `ty_completion_eval` is executed within CI.
CI will also fail if the individual task results have changed.
To make CI pass, you can just re-run the evaluation locally and commit the results:
```console
cargo r -r -p ty_completion_eval -- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
```
CI fails in this case because it would be best to scrutinize the differences here.
It's possible that the ranking has improved in some measurable way, for example.
(Think of this as if it were a snapshot test.)
[mean reciprocal rank]: https://en.wikipedia.org/wiki/Mean_reciprocal_rank

View File

@@ -0,0 +1,17 @@
name,file,index,rank
higher-level-symbols-preferred,main.py,0,
higher-level-symbols-preferred,main.py,1,1
import-deprioritizes-dunder,main.py,0,195
import-deprioritizes-sunder,main.py,0,195
internal-typeshed-hidden,main.py,0,43
numpy-array,main.py,0,
numpy-array,main.py,1,32
object-attr-instance-methods,main.py,0,7
object-attr-instance-methods,main.py,1,1
raise-uses-base-exception,main.py,0,42
scope-existing-over-new-import,main.py,0,495
scope-prioritize-closer,main.py,0,152
scope-simple-long-identifier,main.py,0,140
ty-extensions-lower-stdlib,main.py,0,142
type-var-typing-over-ast,main.py,0,65
type-var-typing-over-ast,main.py,1,353
1 name file index rank
2 higher-level-symbols-preferred main.py 0
3 higher-level-symbols-preferred main.py 1 1
4 import-deprioritizes-dunder main.py 0 195
5 import-deprioritizes-sunder main.py 0 195
6 internal-typeshed-hidden main.py 0 43
7 numpy-array main.py 0
8 numpy-array main.py 1 32
9 object-attr-instance-methods main.py 0 7
10 object-attr-instance-methods main.py 1 1
11 raise-uses-base-exception main.py 0 42
12 scope-existing-over-new-import main.py 0 495
13 scope-prioritize-closer main.py 0 152
14 scope-simple-long-identifier main.py 0 140
15 ty-extensions-lower-stdlib main.py 0 142
16 type-var-typing-over-ast main.py 0 65
17 type-var-typing-over-ast main.py 1 353

View File

@@ -0,0 +1,618 @@
/*!
A simple command line tool for running a completion evaluation.
See `crates/ty_completion_eval/README.md` for examples and more docs.
*/
use std::io::Write;
use std::process::ExitCode;
use std::sync::LazyLock;
use anyhow::{Context, anyhow};
use clap::Parser;
use regex::bytes::Regex;
use ruff_db::files::system_path_to_file;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ty_ide::Completion;
use ty_project::{ProjectDatabase, ProjectMetadata};
use ty_python_semantic::ModuleName;
#[derive(Debug, clap::Parser)]
#[command(
author,
name = "ty_completion_eval",
about = "Run a information retrieval evaluation on ty-powered completions."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, clap::Subcommand)]
enum Command {
/// Run an evaluation on all tasks.
All(AllCommand),
/// Show the completions for a single task.
///
/// This is useful for debugging one single completion task. For
/// example, let's say you make a change to a ranking heuristic and
/// everything looks good except for a few tasks where the rank for
/// the expected answer regressed. Just use this command to run a
/// specific task and you'll get the actual completions for that
/// task printed to stdout.
///
/// If the expected answer is found in the completion list, then
/// it is marked with an `*` along with its rank.
ShowOne(ShowOneCommand),
}
#[derive(Debug, clap::Parser)]
struct AllCommand {
/// The mean reciprocal rank threshold that the evaluation must
/// meet or exceed in order for the evaluation to pass.
#[arg(
long,
help = "The mean reciprocal rank threshold.",
value_name = "FLOAT",
default_value_t = 0.001
)]
threshold: f64,
/// If given, a CSV file of the results for each individual task
/// is written to the path given.
#[arg(
long,
help = "When provided, write individual task results in CSV format.",
value_name = "FILE"
)]
tasks: Option<String>,
/// Whether to keep the temporary evaluation directory around
/// after finishing or not. Keeping it around is useful for
/// debugging when something has gone wrong.
#[arg(
long,
help = "Whether to keep the temporary evaluation directory around or not."
)]
keep_tmp_dir: bool,
}
#[derive(Debug, clap::Parser)]
struct ShowOneCommand {
/// The name of one or more completion tasks to run in isolation.
///
/// The name corresponds to the name of a directory in
/// `./crates/ty_completion_eval/truth/`.
#[arg(help = "The task name to run.", value_name = "TASK_NAME")]
task_name: String,
/// The name of the file, relative to the root of the
/// Python project, that contains one or more completion
/// tasks to run in isolation.
#[arg(long, help = "The file name to run.", value_name = "FILE_NAME")]
file_name: Option<String>,
/// The index of the cursor directive within `file_name`
/// to select.
#[arg(
long,
help = "The index of the cursor directive to run.",
value_name = "INDEX"
)]
index: Option<usize>,
/// Whether to keep the temporary evaluation directory around
/// after finishing or not. Keeping it around is useful for
/// debugging when something has gone wrong.
#[arg(
long,
help = "Whether to keep the temporary evaluation directory around or not."
)]
keep_tmp_dir: bool,
}
impl ShowOneCommand {
fn matches_source_task(&self, task_source: &TaskSource) -> bool {
self.task_name == task_source.name
}
fn matches_task(&self, task: &Task) -> bool {
self.task_name == task.name
&& self
.file_name
.as_ref()
.is_some_and(|name| name == task.cursor_name())
&& self.index.is_some_and(|index| index == task.cursor.index)
}
}
fn main() -> anyhow::Result<ExitCode> {
let args = Cli::parse();
// The base path to which all CLI arguments are relative to.
let cwd = {
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
SystemPathBuf::from_path_buf(cwd).map_err(|path| {
anyhow!(
"The current working directory `{}` contains non-Unicode characters. \
ty only supports Unicode paths.",
path.display()
)
})?
};
// Where we store our truth data.
let truth = cwd.join("crates").join("ty_completion_eval").join("truth");
anyhow::ensure!(
truth.as_std_path().exists(),
"{truth} does not exist: ty's completion evaluation must be run from the root \
of the ruff repository",
truth = truth.as_std_path().display(),
);
// The temporary directory at which we copy our truth
// data to. We do this because we can't use the truth
// data as-is with its `<CURSOR>` annotations (and perhaps
// any other future annotations we add).
let mut tmp_eval_dir = tempfile::Builder::new()
.prefix("ty-completion-eval-")
.tempdir()
.context("Failed to create temporary directory")?;
let tmp_eval_path = SystemPath::from_std_path(tmp_eval_dir.path())
.ok_or_else(|| {
anyhow::anyhow!(
"Temporary directory path is not valid UTF-8: {}",
tmp_eval_dir.path().display()
)
})?
.to_path_buf();
let sources = TaskSource::all(&truth)?;
match args.command {
Command::ShowOne(ref cmd) => {
tmp_eval_dir.disable_cleanup(cmd.keep_tmp_dir);
let Some(source) = sources
.iter()
.find(|source| cmd.matches_source_task(source))
else {
anyhow::bail!("could not find task named `{}`", cmd.task_name);
};
let tasks = source.to_tasks(&tmp_eval_path)?;
let matching: Vec<&Task> = tasks.iter().filter(|task| cmd.matches_task(task)).collect();
anyhow::ensure!(
!matching.is_empty(),
"could not find any tasks matching the given criteria",
);
anyhow::ensure!(
matching.len() < 2,
"found more than one task matching the given criteria",
);
let task = &matching[0];
let completions = task.completions()?;
let mut stdout = std::io::stdout().lock();
for (i, c) in completions.iter().enumerate() {
write!(stdout, "{}", c.name.as_str())?;
if let Some(module_name) = c.module_name {
write!(stdout, " (module: {module_name})")?;
}
if task.cursor.answer.matches(c) {
write!(stdout, " (*, {}/{})", i + 1, completions.len())?;
}
writeln!(stdout)?;
}
writeln!(stdout, "-----")?;
writeln!(stdout, "found {} completions", completions.len())?;
Ok(ExitCode::SUCCESS)
}
Command::All(AllCommand {
threshold,
tasks,
keep_tmp_dir,
}) => {
tmp_eval_dir.disable_cleanup(keep_tmp_dir);
let mut precision_sum = 0.0;
let mut task_count = 0.0f64;
let mut results_wtr = None;
if let Some(ref tasks) = tasks {
let mut wtr = csv::Writer::from_path(SystemPath::new(tasks))?;
wtr.serialize(("name", "file", "index", "rank"))?;
results_wtr = Some(wtr);
}
for source in &sources {
for task in source.to_tasks(&tmp_eval_path)? {
task_count += 1.0;
let completions = task.completions()?;
let rank = task.rank(&completions)?;
precision_sum += rank.map(|rank| 1.0 / f64::from(rank)).unwrap_or(0.0);
if let Some(ref mut wtr) = results_wtr {
wtr.serialize((&task.name, &task.cursor_name(), task.cursor.index, rank))?;
}
}
}
let mrr = precision_sum / task_count;
if let Some(ref mut wtr) = results_wtr {
wtr.flush()?;
}
let mut out = std::io::stdout().lock();
writeln!(out, "mean reciprocal rank: {mrr:.4}")?;
if mrr < threshold {
writeln!(
out,
"Failure: MRR does not exceed minimum threshold of {threshold}"
)?;
Ok(ExitCode::FAILURE)
} else {
writeln!(out, "Success: MRR exceeds minimum threshold of {threshold}")?;
Ok(ExitCode::SUCCESS)
}
}
}
}
/// A single completion task.
///
/// The task is oriented in such a way that we have a single "cursor"
/// position in a Python project. This allows us to ask for completions
/// at that position.
struct Task {
db: ProjectDatabase,
dir: SystemPathBuf,
name: String,
cursor: Cursor,
settings: ty_ide::CompletionSettings,
}
impl Task {
/// Create a new task for the Python project at `project_path`.
///
/// `truth` should correspond to the completion configuration and the
/// expected answer for completions at the given `cursor` position.
fn new(
project_path: &SystemPath,
truth: &CompletionTruth,
cursor: Cursor,
) -> anyhow::Result<Task> {
let name = project_path.file_name().ok_or_else(|| {
anyhow::anyhow!("project directory `{project_path}` does not contain a base name")
})?;
let system = OsSystem::new(project_path);
let mut project_metadata = ProjectMetadata::discover(project_path, &system)?;
project_metadata.apply_configuration_files(&system)?;
let db = ProjectDatabase::new(project_metadata, system)?;
Ok(Task {
db,
dir: project_path.to_path_buf(),
name: name.to_string(),
cursor,
settings: (&truth.settings).into(),
})
}
/// Returns the rank of the expected answer in the completions
/// given.
///
/// The rank is the position (one indexed) at which the expected
/// answer appears in the slice given, or `None` if the answer
/// isn't found at all. A position of zero is maximally correct. A
/// missing position is maximally wrong. Anything in the middle is
/// a grey area with a lower rank being better.
///
/// Because the rank is one indexed, if this returns a rank, then
/// it is guaranteed to be non-zero.
fn rank(&self, completions: &[Completion<'_>]) -> anyhow::Result<Option<u32>> {
completions
.iter()
.position(|completion| self.cursor.answer.matches(completion))
.map(|rank| u32::try_from(rank + 1).context("rank of completion is too big"))
.transpose()
}
/// Return completions for this task.
fn completions(&self) -> anyhow::Result<Vec<Completion<'_>>> {
let file = system_path_to_file(&self.db, &self.cursor.path)
.with_context(|| format!("failed to get database file for `{}`", self.cursor.path))?;
let offset = ruff_text_size::TextSize::try_from(self.cursor.offset).with_context(|| {
format!(
"failed to convert `<CURSOR>` file offset `{}` to 32-bit integer",
self.cursor.offset
)
})?;
let completions = ty_ide::completion(&self.db, &self.settings, file, offset);
Ok(completions)
}
/// Returns the file name, relative to this project's root
/// directory, that contains the cursor directive that we
/// are evaluating.
fn cursor_name(&self) -> &str {
self.cursor
.path
.strip_prefix(&self.dir)
.expect("task directory is a parent of cursor")
.as_str()
}
}
impl std::fmt::Debug for Task {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("Test")
.field("db", &"<ProjectDatabase>")
.field("dir", &self.dir)
.field("name", &self.name)
.field("cursor", &self.cursor)
.field("settings", &self.settings)
.finish()
}
}
/// Truth data for a single completion evaluation test.
#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
struct CompletionTruth {
#[serde(default)]
settings: CompletionSettings,
}
/// Settings to forward to our completion routine.
#[derive(Debug, Default, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
struct CompletionSettings {
#[serde(default)]
auto_import: bool,
}
impl From<&CompletionSettings> for ty_ide::CompletionSettings {
fn from(x: &CompletionSettings) -> ty_ide::CompletionSettings {
ty_ide::CompletionSettings {
auto_import: x.auto_import,
}
}
}
/// The "source" of a task, as found in ty's git repository.
#[derive(Debug)]
struct TaskSource {
/// The directory containing this task.
dir: SystemPathBuf,
/// The name of this task (the basename of `dir`).
name: String,
/// The "truth" data for this task along with any
/// settings. This is pulled from `{dir}/completion.toml`.
truth: CompletionTruth,
}
impl TaskSource {
fn all(src_dir: &SystemPath) -> anyhow::Result<Vec<TaskSource>> {
let mut sources = vec![];
let read_dir = src_dir
.as_std_path()
.read_dir()
.with_context(|| format!("failed to read directory entries in `{src_dir}`"))?;
for result in read_dir {
let dent = result
.with_context(|| format!("failed to get directory entry from `{src_dir}`"))?;
let path = dent.path();
if !path.is_dir() {
continue;
}
let dir = SystemPath::from_std_path(&path).ok_or_else(|| {
anyhow::anyhow!(
"truth source directory `{path}` contains invalid UTF-8",
path = path.display()
)
})?;
sources.push(TaskSource::new(dir)?);
}
// Sort our sources so that we always run in the same order.
// And also so that the CSV output is deterministic across
// all platforms.
sources.sort_by(|source1, source2| source1.name.cmp(&source2.name));
Ok(sources)
}
fn new(dir: &SystemPath) -> anyhow::Result<TaskSource> {
let name = dir.file_name().ok_or_else(|| {
anyhow::anyhow!("truth source directory `{dir}` does not contain a base name")
})?;
let truth_path = dir.join("completion.toml");
let truth_data = std::fs::read(truth_path.as_std_path())
.with_context(|| format!("failed to read truth data at `{truth_path}`"))?;
let truth = toml::from_slice(&truth_data).with_context(|| {
format!("failed to parse TOML completion truth data from `{truth_path}`")
})?;
Ok(TaskSource {
dir: dir.to_path_buf(),
name: name.to_string(),
truth,
})
}
/// Convert this "source" task (from the Ruff repository) into
/// one or more evaluation tasks within a single Python project.
/// Exactly one task is created for each cursor directive found in
/// this source task.
///
/// This includes running `uv sync` to set up a full virtual
/// environment.
fn to_tasks(&self, parent_dst_dir: &SystemPath) -> anyhow::Result<Vec<Task>> {
let dir = parent_dst_dir.join(&self.name);
let cursors = copy_project(&self.dir, &dir)?;
let uv_sync_output = std::process::Command::new("uv")
.arg("sync")
.current_dir(dir.as_std_path())
.output()
.with_context(|| format!("failed to run `uv sync` in `{dir}`"))?;
if !uv_sync_output.status.success() {
let code = uv_sync_output
.status
.code()
.map(|code| code.to_string())
.unwrap_or_else(|| "UNKNOWN".to_string());
let stderr = bstr::BStr::new(&uv_sync_output.stderr);
anyhow::bail!("`uv sync` failed to run with exit code `{code}`, stderr: {stderr}")
}
cursors
.into_iter()
.map(|cursor| Task::new(&dir, &self.truth, cursor))
.collect()
}
}
/// A single cursor directive within a single Python project.
///
/// Each cursor directive looks like:
/// `<CURSOR [expected-module.]expected-symbol>`.
///
/// That is, each cursor directive corresponds to a single completion
/// request, and each request is a single evaluation task.
#[derive(Clone, Debug)]
struct Cursor {
/// The path to the file containing this directive.
path: SystemPathBuf,
/// The index (starting at 0) of this cursor directive
/// within `path`.
index: usize,
/// The byte offset at which this cursor was located
/// within `path`.
offset: usize,
/// The expected symbol (and optionally module) for this
/// completion request.
answer: CompletionAnswer,
}
/// The answer for a single completion request.
#[derive(Clone, Debug, Default, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
struct CompletionAnswer {
symbol: String,
module: Option<String>,
}
impl CompletionAnswer {
/// Returns true when this answer matches the completion given.
fn matches(&self, completion: &Completion) -> bool {
self.symbol == completion.name.as_str()
&& self.module.as_deref() == completion.module_name.map(ModuleName::as_str)
}
}
/// Copy the Python project from `src_dir` to `dst_dir`.
///
/// This also looks for occurrences of cursor directives among the
/// project files and returns them. The original cursor directives are
/// deleted.
///
/// Hidden files or directories are skipped.
///
/// # Errors
///
/// Any underlying I/O errors are bubbled up. Also, if no cursor
/// directives are found, then an error is returned. This guarantees
/// that the `Vec<Cursor>` is always non-empty.
fn copy_project(src_dir: &SystemPath, dst_dir: &SystemPath) -> anyhow::Result<Vec<Cursor>> {
std::fs::create_dir_all(dst_dir).with_context(|| dst_dir.to_string())?;
let mut cursors = vec![];
for result in walkdir::WalkDir::new(src_dir.as_std_path()) {
let dent =
result.with_context(|| format!("failed to get directory entry from {src_dir}"))?;
if dent
.file_name()
.to_str()
.is_some_and(|name| name.starts_with('.'))
{
continue;
}
let src = SystemPath::from_std_path(dent.path()).ok_or_else(|| {
anyhow::anyhow!("path `{}` is not valid UTF-8", dent.path().display())
})?;
let name = src
.strip_prefix(src_dir)
.expect("descendent of `src_dir` must start with `src`");
// let name = src
// .file_name()
// .ok_or_else(|| anyhow::anyhow!("path `{src}` is missing a basename"))?;
let dst = dst_dir.join(name);
if dent.file_type().is_dir() {
std::fs::create_dir_all(dst.as_std_path())
.with_context(|| format!("failed to create directory `{dst}`"))?;
} else {
cursors.extend(copy_file(src, &dst)?);
}
}
anyhow::ensure!(
!cursors.is_empty(),
"could not find any `<CURSOR>` directives in any of the files in `{src_dir}`",
);
Ok(cursors)
}
/// Copies `src` to `dst` while looking for cursor directives.
///
/// Each cursor directive looks like:
/// `<CURSOR [expected-module.]expected-symbol>`.
///
/// When occurrences of cursor directives are found, then they are
/// replaced with the empty string. The position of each occurrence is
/// recorded, which points to the correct place in a document where all
/// cursor directives are omitted.
///
/// # Errors
///
/// When an underlying I/O error occurs.
fn copy_file(src: &SystemPath, dst: &SystemPath) -> anyhow::Result<Vec<Cursor>> {
static RE: LazyLock<Regex> = LazyLock::new(|| {
// Our module/symbol identifier regex here is certainly more
// permissive than necessary, but I think that should be fine
// for this silly little syntax. ---AG
Regex::new(r"<CURSOR:\s*(?:(?<module>[\S--.]+)\.)?(?<symbol>[\S--.]+)>").unwrap()
});
let src_data =
std::fs::read(src).with_context(|| format!("failed to read `{src}` for copying"))?;
let mut cursors = vec![];
// The new data, without cursor directives.
let mut new = Vec::with_capacity(src_data.len());
// An index into `src_data` corresponding to either the start of
// the data or the end of the previous cursor directive that we
// found.
let mut prev_match_end = 0;
// The total bytes removed so far by replacing cursor directives
// with empty strings.
let mut bytes_removed = 0;
for (index, caps) in RE.captures_iter(&src_data).enumerate() {
let overall = caps.get(0).expect("zeroth group is always available");
new.extend_from_slice(&src_data[prev_match_end..overall.start()]);
prev_match_end = overall.end();
let offset = overall.start() - bytes_removed;
bytes_removed += overall.len();
let symbol = str::from_utf8(&caps["symbol"])
.context("expected symbol in cursor directive in `{src}` is not valid UTF-8")?
.to_string();
let module = caps
.name("module")
.map(|module| {
str::from_utf8(module.as_bytes())
.context("expected module in cursor directive in `{src}` is not valid UTF-8")
})
.transpose()?
.map(ToString::to_string);
let answer = CompletionAnswer { symbol, module };
cursors.push(Cursor {
path: dst.to_path_buf(),
index,
offset,
answer,
});
}
new.extend_from_slice(&src_data[prev_match_end..]);
std::fs::write(dst, &new)
.with_context(|| format!("failed to write contents of `{src}` to `{dst}`"))?;
Ok(cursors)
}

View File

@@ -0,0 +1,11 @@
This directory contains truth data for ty's completion evaluation.
# Adding new truth data
To add new truth data, you can either add a new `<CURSOR>` directive to an
existing Python project in this directory or create a new Python project. To
create a new directory, just `cp -a existing new` and modify it as needed. Then:
1. Check `completion.toml` for relevant settings.
2. Run `uv.lock` after updating `pyproject.toml` (if necessary) to ensure the
dependency versions are locked.

View File

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

View File

@@ -0,0 +1,9 @@
# This is similar to the `numpy-array` test case,
# where the completions returned don't contain
# the expected symbol at all.
ZQZQZQ_<CURSOR: sub1.ZQZQZQ_SOMETHING_IMPORTANT>
import sub1
# This works though, so ty sees the symbol where
# as our auto-import symbol finder does not.
sub1.ZQZQZQ_<CURSOR: ZQZQZQ_SOMETHING_IMPORTANT>

View File

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

View File

@@ -0,0 +1 @@
from .sub2 import ZQZQZQ_SOMETHING_IMPORTANT

View File

@@ -0,0 +1 @@
ZQZQZQ_SOMETHING_IMPORTANT = 1

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
# This checks that we prioritize modules without
# preceding double underscores over modules with
# preceding double underscores.
import zqzq<CURSOR: zqzqzq>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
# This checks that we prioritize modules without
# preceding underscores over modules with
# preceding underscores.
import zqzq<CURSOR: zqzqzq>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
# This is a case where a symbol from an internal module appears
# before the desired symbol from `typing`.
#
# We use a slightly different example than the one reported in
# the issue to capture the deficiency via ranking. That is, in
# astral-sh/ty#1274, the (current) top suggestion is the correct one.
#
# ref: https://github.com/astral-sh/ty/issues/1274#issuecomment-3345923575
NoneTy<CURSOR: types.NoneType>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
# This one is tricky because `array` is an exported
# symbol in a whole bunch of numpy internal modules.
#
# At time of writing (2025-10-07), the right completion
# doesn't actually show up at all in the suggestions
# returned. In fact, nothing from the top-level `numpy`
# module shows up.
arra<CURSOR: numpy.array>
import numpy as np
# In contrast to above, this *does* include the correct
# completion. So there is likely some kind of bug in our
# symbol discovery code for auto-import that isn't present
# when using ty to discover symbols (which is likely far
# too expensive to use across all dependencies).
np.arra<CURSOR: array>

View File

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

View File

@@ -0,0 +1,66 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "numpy"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" },
{ url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" },
{ url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" },
{ url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" },
{ url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" },
{ url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" },
{ url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" },
{ url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" },
{ url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" },
{ url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" },
{ url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" },
{ url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" },
{ url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" },
{ url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" },
{ url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" },
{ url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" },
{ url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" },
{ url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" },
{ url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" },
{ url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" },
{ url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" },
{ url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" },
{ url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" },
{ url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" },
{ url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" },
{ url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" },
{ url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" },
{ url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" },
{ url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" },
{ url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" },
{ url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" },
{ url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" },
{ url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" },
{ url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" },
{ url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" },
{ url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" },
{ url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" },
{ url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" },
]
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "numpy" },
]
[package.metadata]
requires-dist = [{ name = "numpy", specifier = ">=2.3.3" }]

View File

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

View File

@@ -0,0 +1,16 @@
class Quux:
def __init__(self): pass
def lion(self): pass
def tiger(self): pass
def bear(self): pass
def chicken(self): pass
def turkey(self): pass
def wasp(self): pass
def rabbit(self): pass
def squirrel(self): pass
quux = Quux()
quux.tur<CURSOR: turkey>
quux = Quux()
quux.be<CURSOR: bear>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
# ref: https://github.com/astral-sh/ty/issues/1262
raise NotImplement<CURSOR: NotImplementedError>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
# ref: https://github.com/astral-sh/ty/issues/1274#issuecomment-3345942698
from typing import Iterator
Iter<CURSOR: Iterator>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
zqzqzq_global_identifier = 1
def foo():
zqzqzq_local_identifier = 1
zqzqzq_<CURSOR: zqzqzq_local_identifier>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
simple_long_identifier = 1
simple<CURSOR: simple_long_identifier>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
# ref: https://github.com/astral-sh/ty/issues/1274#issuecomment-3345879257
reveal<CURSOR: typing.reveal_type>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1732,6 +1732,7 @@ C.<CURSOR>
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: int
mro :: bound method <class 'C'>.mro() -> list[type]
__annotate__ :: @Todo | None
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
@@ -1797,7 +1798,7 @@ Meta.<CURSOR>
// whether we're in release mode or not. These differences
// aren't really relevant for completion tests AFAIK, so
// just redact them. ---AG
filters => [(r"(?m)\s*__(annotations|new)__.+$", "")]},
filters => [(r"(?m)\s*__(annotations|new|annotate)__.+$", "")]},
{
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: property
@@ -1908,6 +1909,7 @@ Quux.<CURSOR>
some_method :: def some_method(self) -> int
some_property :: property
some_static_method :: def some_static_method(self) -> int
__annotate__ :: @Todo | None
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
@@ -1970,7 +1972,7 @@ Answer.<CURSOR>
insta::with_settings!({
// See above: filter out some members which contain @Todo types that are
// rendered differently in release mode.
filters => [(r"(?m)\s*__(call|reduce_ex)__.+$", "")]},
filters => [(r"(?m)\s*__(call|reduce_ex|annotate|signature)__.+$", "")]},
{
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
NO :: Literal[Answer.NO]
@@ -2020,7 +2022,6 @@ Answer.<CURSOR>
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
__ror__ :: bound method <class 'Answer'>.__ror__(value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__signature__ :: bound method <class 'Answer'>.__signature__() -> str
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: bound method <class 'Answer'>.__subclasscheck__(subclass: type, /) -> bool

View File

@@ -15,8 +15,10 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::HasDefinition;
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::definitions_for_keyword_argument;
use ty_python_semantic::types::{Type, call_signature_details};
use ty_python_semantic::types::Type;
use ty_python_semantic::types::ide_support::{
call_signature_details, definitions_for_keyword_argument,
};
use ty_python_semantic::{
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
};

View File

@@ -308,26 +308,8 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:4:1
|
2 | from typing_extensions import TypeAliasType
3 |
4 | Alias = TypeAliasType("Alias", tuple[int, int])
| ^^^^^
5 |
6 | Alias
|
info: Source
--> main.py:6:1
|
4 | Alias = TypeAliasType("Alias", tuple[int, int])
5 |
6 | Alias
| ^^^^^
|
"#);
// TODO: This should jump to the definition of `Alias` above.
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
}
#[test]

View File

@@ -6,7 +6,8 @@ use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
use ruff_python_ast::{AnyNodeRef, Expr, Stmt};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::types::{Type, inlay_hint_function_argument_details};
use ty_python_semantic::types::Type;
use ty_python_semantic::types::ide_support::inlay_hint_function_argument_details;
use ty_python_semantic::{HasType, SemanticModel};
#[derive(Debug, Clone)]

View File

@@ -13,9 +13,8 @@ use ruff_python_ast::{
use ruff_text_size::{Ranged, TextLen, TextRange};
use std::ops::Deref;
use ty_python_semantic::{
HasType, SemanticModel,
semantic_index::definition::DefinitionKind,
types::{Type, definition_kind_for_name},
HasType, SemanticModel, semantic_index::definition::DefinitionKind, types::Type,
types::ide_support::definition_kind_for_name,
};
// This module walks the AST and collects a set of "semantic tokens" for a file

View File

@@ -17,7 +17,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::SemanticModel;
use ty_python_semantic::semantic_index::definition::Definition;
use ty_python_semantic::types::{
use ty_python_semantic::types::ide_support::{
CallSignatureDetails, call_signature_details, find_active_signature_from_details,
};

View File

@@ -520,8 +520,8 @@ pub struct EnvironmentOptions {
/// to reflect the differing contents of the standard library across Python versions.
#[serde(skip_serializing_if = "Option::is_none")]
#[option(
default = r#""3.13""#,
value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | <major>.<minor>"#,
default = r#""3.14""#,
value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | <major>.<minor>"#,
example = r#"
python-version = "3.12"
"#

View File

@@ -301,6 +301,30 @@ reveal_type(Container("a")) # revealed: Container[str]
reveal_type(Container(b"a")) # revealed: Container[bytes]
```
## Implicit self for classes with a default value for their generic parameter
```py
from typing import Self, TypeVar, Generic
class Container[T = bytes]:
def method(self) -> Self:
return self
def _(c: Container[str], d: Container):
reveal_type(c.method()) # revealed: Container[str]
reveal_type(d.method()) # revealed: Container[bytes]
T = TypeVar("T", default=bytes)
class LegacyContainer(Generic[T]):
def method(self) -> Self:
return self
def _(c: LegacyContainer[str], d: LegacyContainer):
reveal_type(c.method()) # revealed: LegacyContainer[str]
reveal_type(d.method()) # revealed: LegacyContainer[bytes]
```
## Invalid Usage
`Self` cannot be used in the signature of a function or variable.

View File

@@ -130,13 +130,9 @@ type IntList = list[int]
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[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[Unknown | str]` is not assignable to `list[LiteralString]`"
o: list[typing.LiteralString] = ["a", "b", "c"]
reveal_type(o) # revealed: list[LiteralString]
@@ -160,6 +156,81 @@ a: list[str] = [1, 2, 3]
b: set[int] = {1, 2, "3"}
```
## Literal annnotations are respected
```toml
[environment]
python-version = "3.12"
```
```py
from enum import Enum
from typing_extensions import Literal, LiteralString
a: list[Literal[1]] = [1]
reveal_type(a) # revealed: list[Literal[1]]
b: list[Literal[True]] = [True]
reveal_type(b) # revealed: list[Literal[True]]
c: list[Literal["a"]] = ["a"]
reveal_type(c) # revealed: list[Literal["a"]]
d: list[LiteralString] = ["a", "b", "c"]
reveal_type(d) # revealed: list[LiteralString]
e: list[list[Literal[1]]] = [[1]]
reveal_type(e) # revealed: list[list[Literal[1]]]
class Color(Enum):
RED = "red"
f: dict[list[Literal[1]], list[Literal[Color.RED]]] = {[1]: [Color.RED, Color.RED]}
reveal_type(f) # revealed: dict[list[Literal[1]], list[Literal[Color.RED]]]
class X[T]:
def __init__(self, value: T): ...
g: X[Literal[1]] = X(1)
reveal_type(g) # revealed: X[Literal[1]]
h: X[int] = X(1)
reveal_type(h) # revealed: X[int]
i: dict[list[X[Literal[1]]], set[Literal[b"a"]]] = {[X(1)]: {b"a"}}
reveal_type(i) # revealed: dict[list[X[Literal[1]]], set[Literal[b"a"]]]
j: list[Literal[1, 2, 3]] = [1, 2, 3]
reveal_type(j) # revealed: list[Literal[1, 2, 3]]
k: list[Literal[1] | Literal[2] | Literal[3]] = [1, 2, 3]
reveal_type(k) # revealed: list[Literal[1, 2, 3]]
type Y[T] = list[T]
l: Y[Y[Literal[1]]] = [[1]]
reveal_type(l) # revealed: list[list[Literal[1]]]
m: list[tuple[Literal[1], Literal[2], Literal[3]]] = [(1, 2, 3)]
reveal_type(m) # revealed: list[tuple[Literal[1], Literal[2], Literal[3]]]
n: list[tuple[int, str, int]] = [(1, "2", 3), (4, "5", 6)]
reveal_type(n) # revealed: list[tuple[int, str, int]]
o: list[tuple[Literal[1], ...]] = [(1, 1, 1)]
reveal_type(o) # revealed: list[tuple[Literal[1], ...]]
p: list[tuple[int, ...]] = [(1, 1, 1)]
reveal_type(p) # revealed: list[tuple[int, ...]]
# literal promotion occurs based on assignability, an exact match is not required
q: list[int | Literal[1]] = [1]
reveal_type(q) # revealed: list[int]
r: list[Literal[1, 2, 3, 4]] = [1, 2]
reveal_type(r) # revealed: list[Literal[1, 2, 3, 4]]
```
## PEP-604 annotations are supported
```py

View File

@@ -820,22 +820,30 @@ reveal_type(C().c) # revealed: int
### Inheritance of class/instance attributes
#### Instance variable defined in a base class
```py
class Base:
declared_in_body: int | None = 1
attribute: int | None = 1
base_class_attribute_1: str | None
base_class_attribute_2: str | None
base_class_attribute_3: str | None
redeclared_with_same_type: str | None
redeclared_with_narrower_type: str | None
redeclared_with_wider_type: str | None
overwritten_in_subclass_body: str
overwritten_in_subclass_method: str
undeclared = "base"
def __init__(self) -> None:
self.defined_in_init: str | None = "value in base"
self.pure_attribute: str | None = "value in base"
self.pure_overwritten_in_subclass_body: str = "value in base"
self.pure_overwritten_in_subclass_method: str = "value in base"
self.pure_undeclared = "base"
class Intermediate(Base):
# Redeclaring base class attributes with the *same *type is fine:
base_class_attribute_1: str | None = None
redeclared_with_same_type: str | None = None
# Redeclaring them with a *narrower type* is unsound, because modifications
# through a `Base` reference could violate that constraint.
@@ -847,22 +855,70 @@ class Intermediate(Base):
# enabled by default can still be discussed.
#
# TODO: This should be an error
base_class_attribute_2: str
redeclared_with_narrower_type: str
# Redeclaring attributes with a *wider type* directly violates LSP.
#
# In this case, both mypy and pyright report an error.
#
# TODO: This should be an error
base_class_attribute_3: str | int | None
redeclared_with_wider_type: str | int | None
# TODO: This should be an `invalid-assignment` error
overwritten_in_subclass_body = None
# TODO: This should be an `invalid-assignment` error
pure_overwritten_in_subclass_body = None
undeclared = "intermediate"
def set_attributes(self) -> None:
# TODO: This should be an `invalid-assignment` error
self.overwritten_in_subclass_method = None
# TODO: This should be an `invalid-assignment` error
self.pure_overwritten_in_subclass_method = None
self.pure_undeclared = "intermediate"
class Derived(Intermediate): ...
reveal_type(Derived.declared_in_body) # revealed: int | None
reveal_type(Derived.attribute) # revealed: int | None
reveal_type(Derived().attribute) # revealed: int | None
reveal_type(Derived().declared_in_body) # revealed: int | None
reveal_type(Derived.redeclared_with_same_type) # revealed: str | None
reveal_type(Derived().redeclared_with_same_type) # revealed: str | None
reveal_type(Derived().defined_in_init) # revealed: str | None
# TODO: It would probably be more consistent if these were `str | None`
reveal_type(Derived.redeclared_with_narrower_type) # revealed: str
reveal_type(Derived().redeclared_with_narrower_type) # revealed: str
# TODO: It would probably be more consistent if these were `str | None`
reveal_type(Derived.redeclared_with_wider_type) # revealed: str | int | None
reveal_type(Derived().redeclared_with_wider_type) # revealed: str | int | None
# TODO: Both of these should be `str`
reveal_type(Derived.overwritten_in_subclass_body) # revealed: Unknown | None
reveal_type(Derived().overwritten_in_subclass_body) # revealed: Unknown | None | str
# TODO: Both of these should be `str`
reveal_type(Derived.overwritten_in_subclass_method) # revealed: str
reveal_type(Derived().overwritten_in_subclass_method) # revealed: str | Unknown | None
reveal_type(Derived().pure_attribute) # revealed: str | None
# TODO: This should be `str`
reveal_type(Derived().pure_overwritten_in_subclass_body) # revealed: Unknown | None | str
# TODO: This should be `str`
reveal_type(Derived().pure_overwritten_in_subclass_method) # revealed: Unknown | None
# TODO: Both of these should be `Unknown | Literal["intermediate", "base"]`
reveal_type(Derived.undeclared) # revealed: Unknown | Literal["intermediate"]
reveal_type(Derived().undeclared) # revealed: Unknown | Literal["intermediate"]
# TODO: This should be `Unknown | Literal["intermediate", "base"]`
reveal_type(Derived().pure_undeclared) # revealed: Unknown | Literal["intermediate"]
```
## Accessing attributes on class objects

View File

@@ -1210,11 +1210,7 @@ from typing_extensions import LiteralString
def f(a: Foo, b: list[str], c: list[LiteralString], e):
reveal_type(e) # revealed: Unknown
# TODO: we should select the second overload here and reveal `str`
# (the incorrect result is due to missing logic in protocol subtyping/assignability)
reveal_type(a.join(b)) # revealed: LiteralString
reveal_type(a.join(b)) # revealed: str
reveal_type(a.join(c)) # revealed: LiteralString
# since both overloads match and they have return types that are not equivalent,

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