Compare commits

..

110 Commits

Author SHA1 Message Date
Douglas Creager
7c976dc570 Merge branch 'main' into dcreager/function-enum
* main:
  Update pre-commit dependencies (#17506)
  [red-knot] Simplify visibility constraint handling for `*`-import definitions (#17486)
  [red-knot] Detect (some) invalid protocols (#17488)
  [red-knot] Correctly identify protocol classes (#17487)
  Update dependency ruff to v0.11.6 (#17516)
  Update Rust crate shellexpand to v3.1.1 (#17512)
  Update Rust crate proc-macro2 to v1.0.95 (#17510)
  Update Rust crate rand to v0.9.1 (#17511)
  Update Rust crate libc to v0.2.172 (#17509)
  Update Rust crate jiff to v0.2.9 (#17508)
  Update Rust crate clap to v4.5.37 (#17507)
  Update astral-sh/setup-uv action to v5.4.2 (#17504)
  Update taiki-e/install-action digest to 09dc018 (#17503)
  [red-knot] infer attribute assignments bound in comprehensions (#17396)
  [red-knot] simplify gradually-equivalent types out of unions and intersections (#17467)
  [red-knot] pull primer projects to run from file (#17473)
2025-04-21 13:18:36 -04:00
Douglas Creager
b44fb47f25 create generic context lazily 2025-04-21 13:15:09 -04:00
renovate[bot]
a4531bf865 Update pre-commit dependencies (#17506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-04-21 17:09:54 +01:00
Alex Waygood
be54b840e9 [red-knot] Simplify visibility constraint handling for *-import definitions (#17486) 2025-04-21 15:33:35 +00:00
Alex Waygood
45b5dedee2 [red-knot] Detect (some) invalid protocols (#17488) 2025-04-21 16:24:19 +01:00
Alex Waygood
9ff4772a2c [red-knot] Correctly identify protocol classes (#17487) 2025-04-21 16:17:06 +01:00
renovate[bot]
c077b109ce Update dependency ruff to v0.11.6 (#17516) 2025-04-21 09:49:22 +01:00
renovate[bot]
8a2dd01db4 Update Rust crate shellexpand to v3.1.1 (#17512) 2025-04-21 01:59:02 +00:00
renovate[bot]
f888e51a34 Update Rust crate proc-macro2 to v1.0.95 (#17510) 2025-04-20 21:57:44 -04:00
renovate[bot]
d11e959ad5 Update Rust crate rand to v0.9.1 (#17511) 2025-04-21 01:57:27 +00:00
renovate[bot]
a56eef444a Update Rust crate libc to v0.2.172 (#17509) 2025-04-20 21:51:51 -04:00
renovate[bot]
14ff67fd46 Update Rust crate jiff to v0.2.9 (#17508) 2025-04-20 21:51:31 -04:00
renovate[bot]
ada7d4da0d Update Rust crate clap to v4.5.37 (#17507) 2025-04-20 21:51:27 -04:00
renovate[bot]
4cafb44ba7 Update astral-sh/setup-uv action to v5.4.2 (#17504)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | patch | `v5.4.1` -> `v5.4.2` |

---

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

---

### Release Notes

<details>
<summary>astral-sh/setup-uv (astral-sh/setup-uv)</summary>

###
[`v5.4.2`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v5.4.2):
🌈 Make sure uv installed by setup-uv is first in PATH

[Compare
Source](https://redirect.github.com/astral-sh/setup-uv/compare/v5.4.1...v5.4.2)

##### Changes

This release fixes an issue on self-hosted runners.
If you manually installed uv with version 0.5.0 or later this version
would overwrite the uv version installed by this action.
We now make sure the version installed by this action is the first found
in PATH

##### 🐛 Bug fixes

- Make sure uv installed by setup-uv is first in PATH
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;373](https://redirect.github.com/astral-sh/setup-uv/issues/373))

##### 🧰 Maintenance

- chore: update known checksums for 0.6.14
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;366](https://redirect.github.com/astral-sh/setup-uv/issues/366))
- chore: update known checksums for 0.6.13
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;365](https://redirect.github.com/astral-sh/setup-uv/issues/365))
- chore: update known checksums for 0.6.12
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;362](https://redirect.github.com/astral-sh/setup-uv/issues/362))
- chore: update known checksums for 0.6.11
@&#8203;[github-actions\[bot\]](https://redirect.github.com/apps/github-actions)
([#&#8203;357](https://redirect.github.com/astral-sh/setup-uv/issues/357))

##### 📚 Documentation

- Fix pep440 identifier instead of specifier
[@&#8203;eifinger](https://redirect.github.com/eifinger)
([#&#8203;358](https://redirect.github.com/astral-sh/setup-uv/issues/358))

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-20 21:50:39 -04:00
renovate[bot]
1445836872 Update taiki-e/install-action digest to 09dc018 (#17503) 2025-04-20 21:50:26 -04:00
Shunsuke Shibayama
da6b68cb58 [red-knot] infer attribute assignments bound in comprehensions (#17396)
## Summary

This PR is a follow-up to #16852.

Instance variables bound in comprehensions are recorded, allowing type
inference to work correctly.

This required adding support for unpacking in comprehension which
resolves https://github.com/astral-sh/ruff/issues/15369.

## Test Plan

One TODO in `mdtest/attributes.md` is now resolved, and some new test
cases are added.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-04-19 06:42:48 +05:30
Carl Meyer
2a478ce1b2 [red-knot] simplify gradually-equivalent types out of unions and intersections (#17467)
## Summary

If two types are gradually-equivalent, that means they share the same
set of possible materializations. There's no need to keep two such types
in the same union or intersection; we should simplify them.

Fixes https://github.com/astral-sh/ruff/issues/17465

The one downside here is that now we will simplify e.g. `Unknown |
Todo(...)` to just `Unknown`, if `Unknown` was added to the union first.
This is correct from a type perspective (they are equivalent types), but
it can mean we lose visibility into part of the cause for the type
inferring as unknown. I think this is OK, but if we think it's important
to avoid this, I can add a special case to try to preserve `Todo` over
`Unknown`, if we see them both in the same union or intersection.

## Test Plan

Added and updated mdtests.
2025-04-18 15:08:57 -07:00
Carl Meyer
8fe2dd5e03 [red-knot] pull primer projects to run from file (#17473)
## Summary

The long line of projects in `mypy_primer.yaml` is hard to work with
when adding projects or checking whether they are currently run. Use a
one-per-line text file instead.

## Test Plan

Ecosystem check on this PR.
2025-04-18 21:20:18 +00:00
Douglas Creager
0a4dec0323 start pulling out enum 2025-04-18 17:04:26 -04:00
Alex Waygood
454ad15aee [red-knot] Fix MRO inference for protocol classes; allow inheritance from subscripted Generic[]; forbid subclassing unsubscripted Generic (#17452) 2025-04-18 19:55:53 +00:00
Hans
fd3fc34a9e [pyflakes] Add fix safety section to docs (F601, F602) (#17440)
## Summary

add fix safety section to repeated_keys_docs, for #15584

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-04-18 18:27:40 +00:00
Hans
c550b4d565 [pyupgrade] Add fix safety section to docs (UP008, UP022) (#17441)
## Summary

add fix safety section to replace_stdout_stderr and
super_call_with_parameters, for #15584
I checked the behavior and found that these two files could only
potentially delete the appended comments, so I submitted them as a PR.
2025-04-18 13:48:13 -04:00
Vasco Schiavo
f8061e8b99 [refurb] Mark the FURB161 fix unsafe except for integers and booleans (#17240)
The PR fixes #16457 .

Specifically, `FURB161` is marked safe, but the rule generates safe
fixes only in specific cases. Therefore, we attempt to mark the fix as
unsafe when we are not in one of these cases.

For instances, the fix is marked as aunsafe just in case of strings (as
pointed out in the issue). Let me know if I should change something.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-04-18 13:46:01 -04:00
Carl Meyer
27a315b740 [red-knot] add fixpoint iteration for Type::member_lookup_with_policy (#17464)
## Summary

Member lookup can be cyclic, with type inference of implicit members. A
sample case is shown in the added mdtest.

There's no clear way to handle such cases other than to fixpoint-iterate
the cycle.

Fixes #17457.

## Test Plan

Added test.
2025-04-18 10:20:03 -07:00
w0nder1ng
08221454f6 [perflint] Implement fix for manual-dict-comprehension (PERF403) (#16719)
## Summary

This change adds an auto-fix for manual dict comprehensions. It also
copies many of the improvements from #13919 (and associated PRs fixing
issues with it), and moves some of the utility functions from
`manual_list_comprehension.rs` into a separate `helpers.rs` to be used
in both.

## Test Plan

I added a preview test case to showcase the new fix and added a test
case in `PERF403.py` to make sure lines with semicolons function. I
didn't yet make similar tests to the ones I added earlier to
`PERF401.py`, but the logic is the same, so it might be good to add
those to make sure they work.
2025-04-18 13:10:40 -04:00
Vasco Schiavo
5fec1039ed [pylint] Make fix unsafe if it deletes comments (PLR1730) (#17459)
The PR addresses issue #17311

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-04-18 12:49:01 -04:00
Douglas Creager
787bcd1c6a [red-knot] Handle explicit class specialization in type expressions (#17434)
You can now use subscript expressions in a type expression to explicitly
specialize generic classes, just like you could already do in value
expressions.

This still does not implement bidirectional checking, so a type
annotation on an assignment does not influence how we infer a
specialization for a (not explicitly specialized) constructor call. You
might get an `invalid-assignment` error if (a) we cannot infer a class
specialization from the constructor call (in which case you end up e.g.
trying to assign `C[Unknown]` to `C[int]`) or if (b) we can infer a
specialization, but it doesn't match the annotation.

Closes https://github.com/astral-sh/ruff/issues/17432
2025-04-18 11:49:22 -04:00
Matthew Mckee
5853eb28dd [red-knot] allow assignment expression in call compare narrowing (#17461)
## Summary

There was some narrowing constraints not covered from the previous PR

```py
def _(x: object):
    if (type(y := x)) is bool:
        reveal_type(y)  # revealed: bool
```

Also, refactored a bit

## Test Plan

Update type_api.md
2025-04-18 08:46:15 -07:00
Carl Meyer
84d064a14c [red-knot] fix building unions with literals and AlwaysTruthy/AlwaysFalsy (#17451)
In #17403 I added a comment asserting that all same-kind literal types
share all the same super-types. This is true, with two notable
exceptions: the types `AlwaysTruthy` and `AlwaysFalsy`. These two types
are super-types of some literal types within a given kind and not
others: `Literal[0]`, `Literal[""]`, and `Literal[b""]` inhabit
`AlwaysFalsy`, while other literals inhabit `AlwaysTruthy`.

This PR updates the literal-unions optimization to handle these types
correctly.

Fixes https://github.com/astral-sh/ruff/issues/17447

Verified locally that `QUICKCHECK_TESTS=100000 cargo test -p
red_knot_python_semantic -- --ignored types::property_tests::stable` now
passes again.
2025-04-18 08:20:03 -07:00
Carl Meyer
e4e405d2a1 [red-knot] Type narrowing for assertions (take 2) (#17345)
## Summary

Fixes #17147.

This was landed in #17149 and then reverted in #17335 because it caused
cycle panics in checking pybind11. #17456 fixed the cause of that panic.

## Test Plan

Add new narrow/assert.md test file

Co-authored-by: Matthew Mckee <matthewmckee04@yahoo.co.uk>
2025-04-18 08:11:07 -07:00
Carl Meyer
1918c61623 [red-knot] class bases are not affected by __future__.annotations (#17456)
## Summary

We were over-conflating the conditions for deferred name resolution.
`from __future__ import annotations` defers annotations, but not class
bases. In stub files, class bases are also deferred. Modeling this
correctly also reduces likelihood of cycles in Python files using `from
__future__ import annotations` (since deferred resolution is inherently
cycle-prone). The same cycles are still possible in `.pyi` files, but
much less likely, since typically there isn't anything in a `pyi` file
that would cause an early return from a scope, or otherwise cause
visibility constraints to persist to end of scope. Usually there is only
code at module global scope and class scope, which can't have `return`
statements, and `raise` or `assert` statements in a stub file would be
very strange. (Technically according to the spec we'd be within our
rights to just forbid a whole bunch of syntax outright in a stub file,
but I kinda like minimizing unnecessary differences between the handling
of Python files and stub files.)

## Test Plan

Added mdtests.
2025-04-18 06:46:21 -07:00
Dhruv Manilawala
44ad201262 [red-knot] Add support for overloaded functions (#17366)
## Summary

Part of #15383, this PR adds support for overloaded callables.

Typing spec: https://typing.python.org/en/latest/spec/overload.html

Specifically, it does the following:
1. Update the `FunctionType::signature` method to return signatures from
a possibly overloaded callable using a new `FunctionSignature` enum
2. Update `CallableType` to accommodate overloaded callable by updating
the inner type to `Box<[Signature]>`
3. Update the relation methods on `CallableType` with logic specific to
overloads
4. Update the display of callable type to display a list of signatures
enclosed by parenthesis
5. Update `CallableTypeOf` special form to recognize overloaded callable
6. Update subtyping, assignability and fully static check to account for
callables (equivalence is planned to be done as a follow-up)

For (2), it is required to be done in this PR because otherwise I'd need
to add some workaround for `into_callable_type` and I though it would be
best to include it in here.

For (2), another possible design would be convert `CallableType` in an
enum with two variants `CallableType::Single` and
`CallableType::Overload` but I decided to go with `Box<[Signature]>` for
now to (a) mirror it to be equivalent to `overload` field on
`CallableSignature` and (b) to avoid any refactor in this PR. This could
be done in a follow-up to better split the two kind of callables.

### Design

There were two main candidates on how to represent the overloaded
definition:
1. To include it in the existing infrastructure which is what this PR is
doing by recognizing all the signatures within the
`FunctionType::signature` method
2. To create a new `Overload` type variant

<details><summary>For context, this is what I had in mind with the new
type variant:</summary>
<p>

```rs
pub enum Type {
	FunctionLiteral(FunctionType),
    Overload(OverloadType),
    BoundMethod(BoundMethodType),
    ...
}

pub struct OverloadType {
	// FunctionLiteral or BoundMethod
    overloads: Box<[Type]>,
	// FunctionLiteral or BoundMethod
    implementation: Option<Type>
}

pub struct BoundMethodType {
    kind: BoundMethodKind,
    self_instance: Type,
}

pub enum BoundMethodKind {
    Function(FunctionType),
    Overload(OverloadType),
}
```

</p>
</details> 

The main reasons to choose (1) are the simplicity in the implementation,
reusing the existing infrastructure, avoiding any complications that the
new type variant has specifically around the different variants between
function and methods which would require the overload type to use `Type`
instead.

### Implementation

The core logic is how to collect all the overloaded functions. The way
this is done in this PR is by recording a **use** on the `Identifier`
node that represents the function name in the use-def map. This is then
used to fetch the previous symbol using the same name. This way the
signatures are going to be propagated from top to bottom (from first
overload to the final overload or the implementation) with each function
/ method. For example:

```py
from typing import overload

@overload
def foo(x: int) -> int: ...
@overload
def foo(x: str) -> str: ...
def foo(x: int | str) -> int | str:
	return x
```

Here, each definition of `foo` knows about all the signatures that comes
before itself. So, the first overload would only see itself, the second
would see the first and itself and so on until the implementation or the
final overload.

This approach required some updates specifically recognizing
`Identifier` node to record the function use because it doesn't use
`ExprName`.

## Test Plan

Update existing test cases which were limited by the overload support
and add test cases for the following cases:
* Valid overloads as functions, methods, generics, version specific
* Invalid overloads as stated in
https://typing.python.org/en/latest/spec/overload.html#invalid-overload-definitions
(implementation will be done in a follow-up)
* Various relation: fully static, subtyping, and assignability (others
in a follow-up)

## Ecosystem changes

_WIP_

After going through the ecosystem changes (there are a lot!), here's
what I've found:

We need assignability check between a callable type and a class literal
because a lot of builtins are defined as classes in typeshed whose
constructor method is overloaded e.g., `map`, `sorted`, `list.sort`,
`max`, `min` with the `key` parameter, `collections.abc.defaultdict`,
etc. (https://github.com/astral-sh/ruff/issues/17343). This makes up
most of the ecosystem diff **roughly 70 diagnostics**. For example:

```py
from collections import defaultdict

# red-knot: No overload of bound method `__init__` matches arguments [lint:no-matching-overload]
defaultdict(int)
# red-knot: No overload of bound method `__init__` matches arguments [lint:no-matching-overload]
defaultdict(list)

class Foo:
    def __init__(self, x: int):
        self.x = x

# red-knot: No overload of function `__new__` matches arguments [lint:no-matching-overload]
map(Foo, ["a", "b", "c"])
```

Duplicate diagnostics in unpacking
(https://github.com/astral-sh/ruff/issues/16514) has **~16
diagnostics**.

Support for the `callable` builtin which requires `TypeIs` support. This
is **5 diagnostics**. For example:
```py
from typing import Any

def _(x: Any | None) -> None:
    if callable(x):
        # red-knot: `Any | None`
        # Pyright: `(...) -> object`
        # mypy: `Any`
        # pyrefly: `(...) -> object`
        reveal_type(x)
```

Narrowing on `assert` which has **11 diagnostics**. This is being worked
on in https://github.com/astral-sh/ruff/pull/17345. For example:
```py
import re

match = re.search("", "")
assert match
match.group()  # error: [possibly-unbound-attribute]
```

Others:
* `Self`: 2
* Type aliases: 6
* Generics: 3
* Protocols: 13
* Unpacking in comprehension: 1
(https://github.com/astral-sh/ruff/pull/17396)

## Performance

Refer to
https://github.com/astral-sh/ruff/pull/17366#issuecomment-2814053046.
2025-04-18 09:57:40 +05:30
Hans
c7372d218d [pyupgrade] Add fix safety section to docs (UP036) (#17444)
## Summary

add fix safety section to outdated_version_block, for #15584
2025-04-17 22:45:53 -04:00
Eric Mark Martin
de8f4e62e2 [red-knot] more type-narrowing in match statements (#17302)
## Summary

Add more narrowing analysis for match statements:
* add narrowing constraints from guard expressions
* add negated constraints from previous predicates and guards to
subsequent cases

This PR doesn't address that guards can mutate your subject, and so
theoretically invalidate some of these narrowing constraints that you've
previously accumulated. Some prior art on this issue [here][mutable
guards].

[mutable guards]:
https://www.irif.fr/~scherer/research/mutable-patterns/mutable-patterns-mlworkshop2024-abstract.pdf

## Test Plan

Add some new tests, and update some existing ones


---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-17 18:18:34 -07:00
Matthew Mckee
edfa03a692 [red-knot] Add some narrowing for assignment expressions (#17448)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

Fixes #14866
Fixes #17437

## Test Plan

Update mdtests in `narrow/`
2025-04-17 17:28:06 -07:00
Alex Waygood
9965cee998 [red-knot] Understand typing.Protocol and typing_extensions.Protocol as equivalent (#17446) 2025-04-17 21:54:22 +01:00
Nuri Jung
58807b2980 Server: Use min instead of max to limit the number of threads (#17421)
## Summary

Prevent overcommit by using max 4 threads as intended.

Unintuitively, `.max()` returns the maximum value of `self` and the
argument (not limiting to the argument). To limit the value to 4, one
needs to use `.min()`.

https://doc.rust-lang.org/std/cmp/trait.Ord.html#method.max
2025-04-18 01:32:12 +05:30
Brent Westbrook
9c47b6dbb0 [red-knot] Detect version-related syntax errors (#16379)
## Summary
This PR extends version-related syntax error detection to red-knot. The
main changes here are:

1. Passing `ParseOptions` specifying a `PythonVersion` to parser calls
2. Adding a `python_version` method to the `Db` trait to make this
possible
3. Converting `UnsupportedSyntaxError`s to `Diagnostic`s
4. Updating existing mdtests  to avoid unrelated syntax errors

My initial draft of (1) and (2) in #16090 instead tried passing a
`PythonVersion` down to every parser call, but @MichaReiser suggested
the `Db` approach instead
[here](https://github.com/astral-sh/ruff/pull/16090#discussion_r1969198407),
and I think it turned out much nicer.

All of the new `python_version` methods look like this:

```rust
fn python_version(&self) -> ruff_python_ast::PythonVersion {
    Program::get(self).python_version(self)
}
```

with the exception of the `TestDb` in `ruff_db`, which hard-codes
`PythonVersion::latest()`.

## Test Plan

Existing mdtests, plus a new mdtest to see at least one of the new
diagnostics.
2025-04-17 14:00:30 -04:00
Hans
d2ebfd6ed7 [pyflakes] Add fix safety section (F841) (#17410)
add fix safety section to docs for #15584, I'm new to ruff and not sure
if the content of this PR is correct, but I hope it can be helpful.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-04-17 09:58:26 -04:00
Alex Waygood
c36f3f5304 [red-knot] Add KnownFunction variants for is_protocol, get_protocol_members and runtime_checkable (#17450) 2025-04-17 14:49:52 +01:00
Brent Westbrook
fcd50a0496 Bump 0.11.6 (#17449) 2025-04-17 09:20:29 -04:00
Shaygan Hooshyari
3ada36b766 Auto generate visit_source_order (#17180)
## Summary

part of: #15655 

I tried generating the source order function using code generation. I
tried a simple approach, but it is not enough to generate all of them
this way.

There is one good thing, that most of the implementations are fine with
this. We only have a few that are not. So one benefit of this PR could
be it eliminates a lot of the code, hence changing the AST structure
will only leave a few places to be fixed.

The `source_order` field determines if a node requires a source order
implementation. If it’s empty it means source order does not visit
anything.

Initially I didn’t want to repeat the field names. But I found two
things:
- `ExprIf` statement unlike other statements does not have the fields
defined in source order. This and also some fields do not need to be
included in the visit. So we just need a way to determine order, and
determine presence.
- Relying on the fields sounds more complicated to me. Maybe another
solution is to add a new attribute `order` to each field? I'm open to
suggestions.
But anyway, except for the `ExprIf` we don't need to write the field
names in order. Just knowing what fields must be visited are enough.

Some nodes had a more complex visitor:

`ExprCompare` required zipping two fields.

`ExprBoolOp` required a match over the fields.

`FstringValue` required a match, I created a new walk_ function that
does the match. and used it in code generation. I don’t think this
provides real value. Because I mostly moved the code from one file to
another. I was tried it as an option. I prefer to leave it in the code
as before.

Some visitors visit a slice of items. Others visit a single element. I
put a check on this in code generation to see if the field requires a
for loop or not. I think better approach is to have a consistent style.
So we can by default loop over any field that is a sequence.

For field types `StringLiteralValue` and `BytesLiteralValue` the types
are not a sequence in toml definition. But they implement `iter` so they
are iterated over. So the code generation does not properly identify
this. So in the code I'm checking for their types.

## Test Plan

All the tests should pass without any changes.
I checked the generated code to make sure it's the same as old code. I'm
not sure if there's a test for the source order visitor.
2025-04-17 08:59:57 -04:00
Alex Waygood
bd89838212 [red-knot] Initial tests for protocols (#17436) 2025-04-17 11:36:41 +00:00
David Peter
b32407b6f3 [red-knot] Dataclasses: synthesize __init__ with proper signature (#17428)
## Summary

This changeset allows us to generate the signature of synthesized
`__init__` functions in dataclasses by analyzing the fields on the class
(and its superclasses). There are certain things that I have not yet
attempted to model in this PR, like `kw_only`,
[`dataclasses.KW_ONLY`](https://docs.python.org/3/library/dataclasses.html#dataclasses.KW_ONLY)
or functionality around
[`dataclasses.field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.field).

ticket: https://github.com/astral-sh/ruff/issues/16651

## Ecosystem analysis

These two seem to depend on missing features in generics (see [relevant
code
here](9898ccbb78/tests/core/test_generics.py (L54))):

> ```diff
> + error[lint:unknown-argument]
/tmp/mypy_primer/projects/dacite/tests/core/test_generics.py:54:24:
Argument `x` does not match any known parameter
> + error[lint:unknown-argument]
/tmp/mypy_primer/projects/dacite/tests/core/test_generics.py:54:38:
Argument `y` does not match any known parameter
> ```



These two are true positives. See [relevant code
here](9898ccbb78/tests/core/test_config.py (L154-L161)).

> ```diff
> + error[lint:invalid-argument-type]
/tmp/mypy_primer/projects/dacite/tests/core/test_config.py:161:24:
Argument to this function is incorrect: Expected `int`, found
`Literal["test"]`
> + error[lint:invalid-argument-type]
/tmp/mypy_primer/projects/dacite/tests/core/test_config.py:172:24:
Argument to this function is incorrect: Expected `int | float`, found
`Literal["test"]`
> ```


This one depends on `**` unpacking of dictionaries, which we don't
support yet:

> ```diff
> + error[lint:missing-argument]
/tmp/mypy_primer/projects/mypy_primer/mypy_primer/globals.py:218:11: No
arguments provided for required parameters `new`, `old`, `repo`,
`type_checker`, `mypyc_compile_level`, `custom_typeshed_repo`,
`new_typeshed`, `old_typeshed`, `new_prepend_path`, `old_prepend_path`,
`additional_flags`, `project_selector`, `known_dependency_selector`,
`local_project`, `expected_success`, `project_date`, `shard_index`,
`num_shards`, `output`, `old_success`, `coverage`, `bisect`,
`bisect_output`, `validate_expected_success`,
`measure_project_runtimes`, `concurrency`, `base_dir`, `debug`, `clear`
> ```



## Test Plan

New Markdown tests.
2025-04-17 09:30:59 +02:00
David Peter
b4de245a5a [red-knot] Dataclasses: support order=True (#17406)
## Summary

Support dataclasses with `order=True`:

```py
@dataclass(order=True)
class WithOrder:
    x: int

WithOrder(1) < WithOrder(2)  # no error
```

Also adds some additional tests to `dataclasses.md`.

ticket: #16651

## Test Plan

New Markdown tests
2025-04-17 08:58:46 +02:00
Douglas Creager
914095d08f [red-knot] Super-basic generic inference at call sites (#17301)
This PR adds **_very_** basic inference of generic typevars at call
sites. It does not bring in a full unification algorithm, and there are
a few TODOs in the test suite that are not discharged by this. But it
handles a good number of useful cases! And the PR does not add anything
that would go away with a more sophisticated constraint solver.

In short, we just look for typevars in the formal parameters, and assume
that the inferred type of the corresponding argument is what that
typevar should map to. If a typevar appears more than once, we union
together the corresponding argument types.

Cases we are not yet handling:

- We are not widening literals.
- We are not recursing into parameters that are themselves generic
aliases.
- We are not being very clever with parameters that are union types.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-16 15:07:36 -04:00
Dhruv Manilawala
5350288d07 [red-knot] Check assignability of bound methods to callables (#17430)
## Summary

This is similar to https://github.com/astral-sh/ruff/pull/17095, it adds
assignability check for bound methods to callables.

## Test Plan

Add test cases to for assignability; specifically it uses gradual types
because otherwise it would just delegate to `is_subtype_of`.
2025-04-17 00:21:59 +05:30
cake-monotone
649610cc98 [red-knot] Support super (#17174)
## Summary

closes #16615 

This PR includes:

- Introduces a new type: `Type::BoundSuper`
- Implements member lookup for `Type::BoundSuper`, resolving attributes
by traversing the MRO starting from the specified class
- Adds support for inferring appropriate arguments (`pivot_class` and
`owner`) for `super()` when it is used without arguments

When `super(..)` appears in code, it can be inferred into one of the
following:

- `Type::Unknown`: when a runtime error would occur (e.g. calling
`super()` out of method scope, or when parameter validation inside
`super` fails)
- `KnownClass::Super::to_instance()`: when the result is an *unbound
super object* or when a dynamic type is used as parameters (MRO
traversing is meaningless)
- `Type::BoundSuper`: the common case, representing a properly
constructed `super` instance that is ready for MRO traversal and
attribute resolution

### Terminology

Python defines the terms *bound super object* and *unbound super
object*.

An **unbound super object** is created when `super` is called with only
one argument (e.g.
`super(A)`). This object may later be bound via the `super.__get__`
method. However, this form is rarely used in practice.

A **bound super object** is created either by calling
`super(pivot_class, owner)` or by using the implicit form `super()`,
where both arguments are inferred from the context. This is the most
common usage.

### Follow-ups

- Add diagnostics for `super()` calls that would result in runtime
errors (marked as TODO)
- Add property tests for `Type::BoundSuper`

## Test Plan

- Added `mdtest/class/super.md`

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-16 18:41:55 +00:00
Wei Lee
1a79722ee0 [airflow] Extend AIR311 rules (#17422)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

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

* Extend the following AIR311 rules
* `airflow.io.path.ObjectStoragePath` → `airflow.sdk.ObjectStoragePath`
    * `airflow.io.storage.attach` → `airflow.sdk.io.attach`
    * `airflow.models.dag.DAG` → `airflow.sdk.DAG`
    * `airflow.models.DAG` → `airflow.sdk.DAG`
    * `airflow.decorators.dag` → `airflow.sdk.dag`
    * `airflow.decorators.task` → `airflow.sdk.task`
    * `airflow.decorators.task_group` → `airflow.sdk.task_group`
    * `airflow.decorators.setup` → `airflow.sdk.setup`
    * `airflow.decorators.teardown` → `airflow.sdk.teardown`

## Test Plan

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

The test case has been added to the button of the existing test
fixtures, confirmed to be correct and later reorgnaized
2025-04-16 12:40:15 -04:00
Carl Meyer
b67590bfde [red-knot] simplify union size limit handling (#17429) 2025-04-16 09:22:16 -07:00
Wei Lee
e6a2de3ac6 [airflow] Extract AIR311 from AIR301 rules (AIR301, AIR311) (#17310)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

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

As discussed in
https://github.com/astral-sh/ruff/issues/14626#issuecomment-2766146129,
we're to separate suggested changes from required changes.

The following symbols have been moved to AIR311 from AIR301. They still
work in Airflow 3.0, but they're suggested to be changed as they're
expected to be removed in a future version.

* arguments
    * `airflow..DAG | dag`
        * `sla_miss_callback`
    * operators
        * `sla`
* name
* `airflow.Dataset] | [airflow.datasets.Dataset` → `airflow.sdk.Asset`
    * `airflow.datasets, rest @ ..`
        * `DatasetAlias` → `airflow.sdk.AssetAlias`
        * `DatasetAll` → `airflow.sdk.AssetAll`
        * `DatasetAny` → `airflow.sdk.AssetAny`
* `expand_alias_to_datasets` → `airflow.sdk.expand_alias_to_assets`
        * `metadata.Metadata` → `airflow.sdk.Metadata`
    <!--airflow.models.baseoperator-->
    * `airflow.models.baseoperator.chain` → `airflow.sdk.chain`
* `airflow.models.baseoperator.chain_linear` →
`airflow.sdk.chain_linear`
* `airflow.models.baseoperator.cross_downstream` →
`airflow.sdk.cross_downstream`
* `airflow.models.baseoperatorlink.BaseOperatorLink` →
`airflow.sdk.definitions.baseoperatorlink.BaseOperatorLink`
    * `airflow.timetables, rest @ ..`
* `datasets.DatasetOrTimeSchedule` → *
`airflow.timetables.assets.AssetOrTimeSchedule`
    * `airflow.utils, rest @ ..`
        <!--airflow.utils.dag_parsing_context-->
* `dag_parsing_context.get_parsing_context` →
`airflow.sdk.get_parsing_context`

## Test Plan

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

The test fixture has been updated acccordingly
2025-04-16 11:06:57 -04:00
Carl Meyer
c7b5067ef8 [red-knot] set a size limit on unions of literals (#17419)
## Summary

Until we optimize our full union/intersection representation to
efficiently handle large numbers of same-kind literal types "as a
block", set a fairly low limit on the size of unions of literals.

We will want to increase this limit once we've made the broader
efficiency improvement (tracked in
https://github.com/astral-sh/ruff/issues/17420).

## Test Plan

`cargo bench --bench red_knot`
2025-04-16 14:23:11 +00:00
Carl Meyer
5a115e750d [red-knot] make large-union benchmark slow again (#17418)
## Summary

Now that we've made the large-unions benchmark fast, let's make it slow
again!

This adds a following operation (checking `len`) on the large union,
which is slow, even though building the large union is now fast. (This
is also observed in a real-world code sample.) It's slow because for
every element of the union, we fetch its `__len__` method and check it
for compatibility with `Sized`.

We can make this fast by extending the grouped-types approach, as
discussed in https://github.com/astral-sh/ruff/pull/17403, so that we
can do this `__len__` operation (which is identical for every literal
string) just once for all literal strings, instead of once per literal
string type in the union.

Until we do that, we can make this acceptably fast again for now by
setting a lowish limit on union size, which we can increase in the
future when we make it fast. This is what I'll do in the next PR.

## Test Plan

`cargo bench --bench red_knot`
2025-04-16 14:05:42 +00:00
Carl Meyer
a1f361949e [red-knot] optimize building large unions of literals (#17403)
## Summary

Special-case literal types in `UnionBuilder` to speed up building large
unions of literals.

This optimization is extremely effective at speeding up building even a
very large union (it improves the large-unions benchmark by 41x!). The
problem we can run into is that it is easy to then run into another
operation on the very large union (for instance, narrowing may add it to
an intersection, which then distributes it over the intersection) which
is still slow.

I think it is possible to avoid this by extending this optimized
"grouped" representation throughout not just `UnionBuilder`, but all of
our union and intersection representations. I have some work in this
direction, but rather than spending more time on it right now, I'd
rather just land this much, along with a limit on the size of these
unions (to avoid building really big unions quickly and then hitting
issues where they are used.)

## Test Plan

Existing tests and benchmarks.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-16 13:55:37 +00:00
Matthew Mckee
13ea4e5d0e [red-knot] Fix comments in type_api.md (#17425) 2025-04-16 11:19:48 +00:00
Matthew Mckee
a2a7b1e268 [red-knot] Do not assume that x != 0 if x inhabits ~Literal[0] (#17370)
## Summary

Fixes incorrect negated type eq and ne assertions in
infer_binary_intersection_type_comparison

fixes #17360

## Test Plan

Remove and update some now incorrect tests
2025-04-15 22:27:27 -07:00
Carl Meyer
1dedcb9e0d [red-knot] make large-union benchmark more challenging (#17416) 2025-04-15 18:04:57 -07:00
Douglas Creager
807a8a7a29 [red-knot] Acknowledge that T & anything is assignable to T (#17413)
This reworks the assignability/subtyping relations a bit to handle
typevars better:

1. For the most part, types are not assignable to typevars, since
there's no guarantee what type the typevar will be specialized to.

2. An intersection is an exception, if it contains the typevar itself as
one of the positive elements. This should fall out from the other
clauses automatically, since a typevar is assignable to itself, and an
intersection is assignable to something if any positive element is
assignable to that something.

3. Constrained typevars are an exception, since they must be specialized
to _exactly_ one of the constraints, not to a _subtype_ of a constraint.
If a type is assignable to every constraint, then the type is also
assignable to the constrained typevar.

We already had a special case for (3), but the ordering of it relative
to the intersection clauses meant we weren't catching (2) correctly. To
fix this, we keep the special case for (3), but fall through to the
other match arms for non-constrained typevars and if the special case
isn't true for a constrained typevar.

Closes https://github.com/astral-sh/ruff/issues/17364
2025-04-15 16:34:07 -04:00
renovate[bot]
78dabc332d Update Rust crate clap to v4.5.36 (#17381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-04-15 16:27:36 +00:00
Dhruv Manilawala
bfc17fecaa Raise syntax error when \ is at end of file (#17409)
## Summary

This PR fixes a bug in the lexer specifically around line continuation
character at end of file.

The reason this was occurring is because the lexer wouldn't check for
EOL _after_ consuming the escaped newline but only if the EOL was right
after the line continuation character.

fixes: #17398 

## Test Plan

Add tests for the scenarios where this should occur mainly (a) when the
state is `AfterNewline` and (b) when the state is `Other`.
2025-04-15 21:26:12 +05:30
cake-monotone
942cb9e3ad [red-knot] Add regression tests for narrowing constraints cycles (#17408)
## Summary

closes #17215 

This PR adds regression tests for the following cycled queries:
- all_narrowing_constraints_for_expression
- all_negative_narrowing_constraints_for_expression

The following test files are included:
-
`red_knot_project/resources/test/corpus/cycle_narrowing_constraints.py`
-
`red_knot_project/resources/test/corpus/cycle_negative_narrowing_constraints.py`

These test names don't follow the existing naming convention based on
Cinder.
However, I’ve chosen these names to clearly reflect the regression
cases.
Let me know if you’d prefer to align more closely with the existing
Cinder-based style.

## Test Plan

```sh
git checkout 1a6a10b30
cargo test --package red_knot_project  -- corpus
```
2025-04-15 07:27:54 -07:00
Alex Waygood
312a487ea7 [red-knot] Add some knowledge of __all__ to *-import machinery (#17373) 2025-04-15 12:56:40 +01:00
renovate[bot]
cf8dc60292 Update taiki-e/install-action digest to be7c31b (#17379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 12:55:31 +01:00
renovate[bot]
ea5d5c4e29 Update Rust crate mimalloc to v0.1.46 (#17382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 12:55:15 +01:00
renovate[bot]
c99d5522eb Update PyO3/maturin-action action to v1.49.1 (#17384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 12:54:53 +01:00
renovate[bot]
e57c83e369 Update Rust crate anyhow to v1.0.98 (#17380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 12:52:03 +01:00
Andrew Gallant
1d49e71ddd dependencies: switch from chrono to jiff
We weren't really using `chrono` for anything other than getting the
current time and formatting it for logs.

Unfortunately, this doesn't quite get us to a point where `chrono`
can be removed. From what I can tell, we're still bringing it via
[`tracing-subscriber`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/)
and
[`quick-junit`](https://docs.rs/quick-junit/latest/quick_junit/).
`tracing-subscriber` does have an
[issue open about Jiff](https://github.com/tokio-rs/tracing/discussions/3128),
but there's no movement on it.

Normally I'd suggest holding off on this since it doesn't get us all of
the way there and it would be better to avoid bringing in two datetime
libraries, but we are, it appears, already there. In particular,
`env_logger` brings in Jiff. So this PR doesn't really make anything
worse, but it does bring us closer to an all-Jiff world.
2025-04-15 07:47:55 -04:00
renovate[bot]
f05b2d3673 Update Rust crate bstr to v1.12.0 (#17385)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [bstr](https://redirect.github.com/BurntSushi/bstr) |
workspace.dependencies | minor | `1.11.3` -> `1.12.0` |

---

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

---

### Release Notes

<details>
<summary>BurntSushi/bstr (bstr)</summary>

###
[`v1.12.0`](https://redirect.github.com/BurntSushi/bstr/compare/1.11.3...1.12.0)

[Compare
Source](https://redirect.github.com/BurntSushi/bstr/compare/1.11.3...1.12.0)

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 07:46:27 -04:00
Alex Waygood
79b921179c [red-knot] Further optimize *-import visibility constraints (#17375) 2025-04-15 12:32:22 +01:00
David Peter
1f85e0d0a0 [red-knot] Minor 'member_lookup_with_policy' fix (#17407)
## Summary

Couldn't really think of a regression test, but it's probably better to
fix this if we ever add new member-lookup-policies.
2025-04-15 13:28:51 +02:00
David Peter
03adae80dc [red-knot] Initial support for dataclasses (#17353)
## Summary

Add very early support for dataclasses. This is mostly to make sure that
we do not emit false positives on dataclass construction, but it also
lies some foundations for future extensions.

This seems like a good initial step to merge to me, as it basically
removes all false positives on dataclass constructor calls. This allows
us to use the ecosystem checks for making sure we don't introduce new
false positives as we continue to work on dataclasses.

## Ecosystem analysis

I re-ran the mypy_primer evaluation of [the `__init__`
PR](https://github.com/astral-sh/ruff/pull/16512) locally with our
current mypy_primer version and project selection. It introduced 1597
new diagnostics. Filtering those by searching for `__init__` and
rejecting those that contain `invalid-argument-type` (those could not
possibly be solved by this PR) leaves 1281 diagnostics. The current
version of this PR removes 1171 diagnostics, which leaves 110
unaccounted for. I extracted the lint + file path for all of these
diagnostics and generated a diff (of diffs), to see which
`__init__`-diagnostics remain. I looked at a subset of these: There are
a lot of `SomeClass(*args)` calls where we don't understand the
unpacking yet (this is not even related to `__init__`). Some others are
related to `NamedTuple`, which we also don't support yet. And then there
are some errors related to `@attrs.define`-decorated classes, which
would probably require support for `dataclass_transform`, which I made
no attempt to include in this PR.

## Test Plan

New Markdown tests.
2025-04-15 10:39:21 +02:00
github-actions[bot]
4894f52bae Sync vendored typeshed stubs (#17402)
Close and reopen this PR to trigger CI

---------

Co-authored-by: typeshedbot <>
Co-authored-by: David Peter <mail@david-peter.de>
2025-04-15 09:16:42 +02:00
Mike Perlov
3b24fe5c07 [red-knot] improve function/bound method type display (#17294)
## Summary

* Partial #17238
* Flyby from discord discussion - `todo_type!` now statically checks for
no parens in the message to avoid issues between debug & release build
tests

## Test Plan

many mdtests are changing
2025-04-14 15:56:18 -07:00
Dhruv Manilawala
b5d529e976 [red-knot] Move relation methods from CallableType to Signature (#17365)
## Summary

This PR moves all the relation methods from `CallableType` to
`Signature`.

The main reason for this is that `Signature` is going to be the common
denominator between normal and overloaded callables and the core logic
to check a certain relationship is going to just require the information
that would exists on `Signature`. For example, to check whether an
overloaded callable is a subtype of a normal callable, we need to check
whether _every_ overloaded signature is a subtype of the normal
callable's signature. This "every" logic would become part of the
`CallableType` and the core logic of checking the subtyping would exists
on `Signature`.
2025-04-15 03:32:25 +05:30
Brent Westbrook
014bb526f4 [syntax-errors] await outside async functions (#17363)
Summary
--

This PR implements detecting the use of `await` expressions outside of
async functions. This is a reimplementation of
[await-outside-async
(PLE1142)](https://docs.astral.sh/ruff/rules/await-outside-async/) as a
semantic syntax error.

Despite the rule name, PLE1142 also applies to `async for` and `async
with`, so these are covered here too.

Test Plan
--

Existing PLE1142 tests.

I also deleted more code from the `SemanticSyntaxCheckerVisitor` to
avoid changes in other parser tests.
2025-04-14 13:01:48 -04:00
Carl Meyer
e2a38e4c00 [red-knot] optimize is_subtype_of for literals (#17394)
## Summary

Allows us to establish that two literals do not have a subtype
relationship with each other, without having to fallback to a typeshed
Instance type, which is comparatively slow.

Improves the performance of the many-string-literals union benchmark by
5x.

## Test Plan

`cargo test -p red_knot_python_semantic` and `cargo bench --bench
red_knot`.
2025-04-14 09:42:44 -07:00
Carl Meyer
9bee9429de [red-knot] add a large-union-of-string-literals benchmark (#17393)
## Summary

Add a benchmark for a large-union case that currently has exponential
blow-up in execution time.

## Test Plan

`cargo bench --bench red_knot`
2025-04-14 09:40:46 -07:00
renovate[bot]
17e5b61c54 Update pre-commit dependencies (#17383) 2025-04-14 17:14:38 +01:00
David Peter
701aecb2a6 [red-knot] mypy_primer: Fail job on panic or internal errors (#17389)
## Summary

Let the mypy_primer job fail if Red Knot panics or exits with code 2
(indicating an internal error).

Corresponding mypy_primer commit:
90808f4656

In addition, we may also want to make a successful mypy_primer run
required for merging?

## Test Plan

Made sure that mypy_primer exits with code 70 locally on panics, which
should result in a pipeline failure, since we only allow code 0 and 1 in
the pipeline here:
a4d7c6669b/.github/workflows/mypy_primer.yaml (L73)
2025-04-14 11:50:17 +00:00
David Peter
850360a0b4 [red-knot] Document limitations of diagnostics-silencing in unreachable code (#17387)
## Summary

Document the limitations of our current approach to silencing only a
subset of diagnostics in unreachable sections.
2025-04-14 12:55:14 +02:00
Shunsuke Shibayama
dfd8eaeb32 [red-knot] detect unreachable attribute assignments (#16852)
## Summary

This PR closes #15967.

Attribute assignments that are statically known to be unreachable are
excluded from consideration for implicit instance attribute type
inference. If none of the assignments are found to be reachable, an
`unresolved-attribute` error is reported.

## Test Plan

[A test
case](https://github.com/astral-sh/ruff/blob/main/crates/red_knot_python_semantic/resources/mdtest/attributes.md#attributes-defined-in-statically-known-to-be-false-branches)
marked as TODO now work as intended, and new test cases have been added.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-04-14 09:23:20 +02:00
Dhruv Manilawala
3aa3ee8b89 [red-knot] Use concise message for the server (#17367)
## Summary

This is mainly a follow-up from
https://github.com/astral-sh/ruff/pull/17357 to use the
`concise_message` method for the red-knot server which I noticed
recently while testing the overload implementation.
2025-04-12 04:49:28 +00:00
David Peter
f0c3abd198 [red-knot] Minor followup on str.startswith (#17362)
## Summary

Minor follow-up to https://github.com/astral-sh/ruff/pull/17351, thank
you @AlexWaygood.
2025-04-11 18:40:08 +00:00
Dhruv Manilawala
e5026c0877 Use concise message to show diagnostics in playground (#17357)
## Summary

This PR fixes the playground to use the new `concise_diagnostic` method.

## Test plan

**Before:**

<img width="1728" alt="Screenshot 2025-04-11 at 11 37 34 AM"
src="https://github.com/user-attachments/assets/cbfcbc52-2e70-4277-9363-ba197711390e"
/>

**After:**

<img width="1728" alt="Screenshot 2025-04-11 at 11 38 03 AM"
src="https://github.com/user-attachments/assets/356ec63c-50d9-49a8-8df4-84000b46fb6d"
/>
2025-04-11 22:44:24 +05:30
Brent Westbrook
da32a83c9f [syntax-errors] return outside function (#17300)
Summary
--

This PR reimplements [return-outside-function
(F706)](https://docs.astral.sh/ruff/rules/return-outside-function/) as a
semantic syntax error.

These changes are very similar to those in
https://github.com/astral-sh/ruff/pull/17298.

Test Plan
--

New linter tests, plus existing F706 tests.
2025-04-11 17:05:54 +00:00
Charlie Marsh
4bfdf54d1a Allow types.ruff.rs for red-knot playground (#17358) 2025-04-11 12:56:03 -04:00
Andrew Gallant
7d11ef1564 red_knot_python_semantic: make TextRange required for reporting a lint diagnostic
This commit shuffles the reporting API around a little bit such that a
range is required, up front, when reporting a lint diagnostic. This in
turn enables us to make suppression checking eager.

In order to avoid callers needing to provide the range twice, we create
a primary annotation *without* a message inside the `Diagnostic`
encapsulated by the guard. We do this instead of requiring the message
up front because we're concerned about API complexity and the effort
involved in creating the message.

In order to provide a means of attaching a message to the primary
annotation, we expose a convenience API on `LintDiagnosticGuard` for
setting the message. This isn't generally possible for a `Diagnostic`,
but since a `LintDiagnosticGuard` knows how the `Diagnostic` was
constructed, we can offer this API correctly.

It strikes me that it might be easy to forget to attach a primary
annotation message, btu I think this the "least" bad failure mode. And
in particular, it should be somewhat obvious that it's missing once one
adds a snapshot test for how the diagnostic renders.

Otherwise, this API gives us the ability to eagerly check whether a
diagnostic should be reported with nearly minimal information. It also
shouldn't have any footguns since it guarantees that the primary
annotation is tied to the file in the typing context. And it keeps
things pretty simple: callers only need to provide what is actually
strictly necessary to make a diagnostic.
2025-04-11 12:36:36 -04:00
Andrew Gallant
b79d43a852 ruff_db: add primary annotation message mutators on Diagnostic
This will enable us to provide an API on `LintDiagnosticGuard` for
setting the primary annotation message. It will require an `unwrap()`,
but due to how `LintDiagnosticGuard` will build a `Diagnostic`, this
`unwrap()` will be guaranteed to succeed. (And it won't bubble out to
every user of `LintDiagnosticGuard`.)
2025-04-11 12:36:36 -04:00
Andrew Gallant
a4d3c4bf8b red_knot_python_semantic: make the guards implement DerefMut
This is the payoff from removing a bit of indirection. The types still
exist, but now callers don't need to do builder -> reporter ->
diagnostic. They can just conceptually think of it as builder ->
diagnostic.
2025-04-11 12:36:36 -04:00
Andrew Gallant
f3cc287a40 red_knot_python_semantic: tweak naming
We're going to make the guards deref to `Diagnostic` in order to remove
a layer of indirection in the reporter API. (Well, technically the layer
is not removed since the types still exist, but in actual _usage_ the
layer will be removed. We'll see how it shakes out in the next commit.)
2025-04-11 12:36:36 -04:00
Andrew Gallant
bd5b8a9ec6 ruff_db: tweak APIs accepting a diagnostic message
This expands the set of types accepted for diagnostic messages. Instead
of only `std::fmt::Display` impls, we now *also* accept a concrete
`DiagnosticMessage`.

This will be useful to avoid unnecessary copies of every single
diagnostic message created via `InferContext::report_lint`. (I'll call
out how this helps in a subsequent commit.)
2025-04-11 12:36:36 -04:00
renovate[bot]
0caf09f5c3 Update dependency vite to v6.2.6 (#17354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 15:56:10 +00:00
David Peter
47956db567 [red-knot] Specialize str.startswith for string literals (#17351)
## Summary

Infer precise Boolean literal types for `str.startswith` calls where the
instance and the prefix are both string literals. This allows us to
understand `sys.platform.startswith(…)` branches.

## Test Plan

New Markdown tests
2025-04-11 16:26:45 +02:00
Brent Westbrook
ffef71d106 [syntax-errors] yield, yield from, and await outside functions (#17298)
Summary
--

This PR reimplements [yield-outside-function
(F704)](https://docs.astral.sh/ruff/rules/yield-outside-function/) as a
semantic syntax error. Despite the name, this rule covers `yield from`
and `await` in addition to `yield`.

Test Plan
--

New linter tests, along with the existing F704 test.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-04-11 10:16:23 -04:00
Micha Reiser
7e571791c0 [red-knot] Refresh diagnostics when changing related files (#17350) 2025-04-11 15:50:28 +02:00
Brent Westbrook
8e11c53310 Add Checker::import_from_typing (#17340)
Summary
--

This PR replaces uses of version-dependent imports from `typing` or
`typing_extensions` with a centralized `Checker::import_from_typing`
method.

The idea here is to make the fix for #9761 (whatever it ends up being)
applicable to all of the rules performing similar checks.

Test Plan
--

Existing tests for the affected rules.
2025-04-11 13:37:55 +00:00
Max Mynter
1aad180aae Don't add chaperone space after escaped quote in triple quote (#17216)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-04-11 10:21:47 +02:00
David Peter
1a3b73720c [red-knot] Silence errors in unreachable type annotations / class bases (#17342)
## Summary

For silencing `invalid-type-form` diagnostics in unreachable code, we
use the same approach that we use before and check the reachability that
we already record.

For silencing `invalid-bases`, we simply check if the type of the base
is `Never`. If so, we silence the diagnostic with the argument that the
class construction would never happen.

## Test Plan

Updated Markdown tests.
2025-04-10 22:47:47 +02:00
David Peter
8b2727cf67 [red-knot] Silence unresolved-import in unreachable code (#17336)
## Summary

Similar to what we did for `unresolved-reference` and
`unresolved-attribute`, we now also silence `unresolved-import`
diagnostics if the corresponding `import` statement is unreachable.

This addresses the (already closed) issue #17049.

## Test Plan

Adapted Markdown tests.
2025-04-10 21:13:28 +02:00
Andrew Gallant
410aa4b8fd red_knot_python_semantic: move TODO comment
It fits with reporting lint diagnostics much better than reporting
general diagnostics.
2025-04-10 13:21:00 -04:00
Andrew Gallant
bd58135b0d red_knot_python_semantic: rename lint() and report()
... to `report_lint()` and `report_diagnostic()`. And rename the old
`report_lint()` to `report_lint_old()`.
2025-04-10 13:21:00 -04:00
Andrew Gallant
81045758d3 ruff_db: use Annotation::get_message in more places
I added this accessor because tests want it, but we can also use it in
other places internally. It's a little nicer because it does the
`as_deref()` for you.
2025-04-10 13:21:00 -04:00
Andrew Gallant
0b2a5b5cd3 red_knot_python_semantic: tweak docs on building reporter builders 2025-04-10 13:21:00 -04:00
Andrew Gallant
7d958a9ee5 red_knot_python_semantic: remove the "old" secondary message type
This finally completes the deletion of all old diagnostic types.

We do this by migrating the second (and last) use of secondary
diagnostic messages: to highlight the return type of a function
definition when its return value is inconsistent with the type.

Like the last diagnostic, we do actually change the message here a bit.
We don't need a sub-diagnostic here, and we can instead just add a
secondary annotation to highlight the return type.
2025-04-10 13:21:00 -04:00
Andrew Gallant
7e2eb591bc red_knot_python_semantic: replace one use of "old" secondary diagnostic messages
This is the first use of the new `lint()` reporter.

I somewhat skipped a step here and also modified the actual diagnostic
message itself. The snapshots should tell the story.

We couldn't do this before because we had no way of differentiating
between "message for the diagnostic as a whole" and "message for a
specific code annotation." Now we can, so we can write more precise
messages based on the assumption that users are also seeing the code
snippet.

The downside here is that the actual message text can become quite vague
in the absence of the code snippet. This occurs, for example, with
concise diagnostic formatting. It's unclear if we should do anything
about it. I don't really see a way to make it better that doesn't
involve creating diagnostics with messages for each mode, which I think
would be a major PITA.

The upside is that this code gets a bit simpler, and we very
specifically avoid doing extra work if this specific lint is disabled.
2025-04-10 13:21:00 -04:00
Andrew Gallant
ba408f4231 red_knot_python_semantic: update revealed type snapshots
This required a bit of surgery in the diagnostic matching and more
faffing about using a "concise" message from a diagnostic instead of
only printing the "primary" message.
2025-04-10 13:21:00 -04:00
Andrew Gallant
28b64064f5 ruff_db: tweak how the revealed type diagnostic is rendered
In the new diagnostic data model, we really should have a main
diagnostic message *and* a primary span (with an optional message
attached to it) for every diagnostic.

In this commit, I try to make this true for the "revealed type"
diagnostic. Instead of the annotation saying both "revealed type is"
and also the revealed type itself, the annotation is now just the
revealed type and the main diagnostic message is "Revealed type."

I expect this may be controversial. I'm open to doing something
different. I tried to avoid redundancy, but maybe this is a special case
where we want the redundancy. I'm honestly not sure. I do *like* how it
looks with this commit, but I'm not working with Red Knot's type
checking daily, so my opinion doesn't count for much.

This did also require some tweaking to concise diagnostic formatting in
order to preserve the essential information.

This commit doesn't update every relevant snapshot. Just a few. I split
the rest out into the next commit.
2025-04-10 13:21:00 -04:00
Andrew Gallant
75b15ea2d0 red_knot: add explicit test for concise reveal_type diagnostic
This test reflects the status quo before we change things.
2025-04-10 13:21:00 -04:00
Andrew Gallant
e7e86b8584 red_knot_python_semantic: remove InferContext::report_diagnostic
... and replace it with use of `report()`.

Interestingly, this is the only instance of `report_diagnostic` used
directly, and thus anticipated to be the only instance of using
`report()`. If this ends up being a true single use method, we could
make it less generic and tailored specifically to "reveal type."

Two other things to note:

I left the "primary message" as empty. This avoids changing snapshots.
I address this in a subsequent commit.

The creation of a diagnostic here is a bit verbose/annoying. Certainly
more so than it was. This is somewhat expected since our diagnostic
model is more expressive and because we don't have a proc macro. I
avoided creating helpers for this case since there's only one use of
`report()`. But I expect to create helpers for the `lint()` case.
2025-04-10 13:21:00 -04:00
Andrew Gallant
f84bc07d56 red_knot_python_semantic: add "reporter" API
This is a surgical change that adds new `report()` and `lint()`
APIs to `InferContext`. These are intended to replace the existing
`report_*` APIs.

The comments should explain what these reporters are meant to do. For
the most part, this is "just" shuffling some code around. The actual
logic for determining whether a lint *should* be reported or not remains
unchanged and we don't make any changes to how a `Diagnostic` is
actually constructed (yet).

I initially tried to just use `LintReporter` and `DiagnosticReporter`
without the builder types, since I perceive the builder types to be an
annoying additional layer. But I found it also exceedingly annoying to
have to construct and provide the diagnostic message before you even
know if you are going to build the diagnostic. I also felt like this
could result in potentially unnecessary and costly querying in some
cases, although this is somewhat hand wavy. So I overall felt like the
builder route was the way to go. If the builders end up being super
annoying, we can probably add convenience APIs for common patterns to
paper over them.
2025-04-10 13:21:00 -04:00
436 changed files with 18317 additions and 8764 deletions

View File

@@ -49,7 +49,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build sdist"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
command: sdist
args: --out dist
@@ -79,7 +79,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
target: x86_64
args: --release --locked --out dist
@@ -121,7 +121,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - aarch64"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
target: aarch64
args: --release --locked --out dist
@@ -177,7 +177,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
target: ${{ matrix.platform.target }}
args: --release --locked --out dist
@@ -230,7 +230,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -304,7 +304,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
target: ${{ matrix.platform.target }}
manylinux: auto
@@ -370,14 +370,14 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --locked --out dist
- name: "Test wheel"
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3
uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3
with:
image: alpine:latest
options: -v ${{ github.workspace }}:/io -w /io
@@ -435,7 +435,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2

View File

@@ -237,13 +237,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-insta
- name: Red-knot mdtests (GitHub annotations)
@@ -291,13 +291,13 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -320,7 +320,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -376,7 +376,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Build"
run: cargo build --release --locked
@@ -401,13 +401,13 @@ jobs:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@v1
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -433,7 +433,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@main
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -455,7 +455,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
name: Download Ruff binary to test
id: download-cached-binary
@@ -641,7 +641,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@main
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -662,7 +662,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
with:
args: --out dist
- name: "Test wheel"
@@ -681,7 +681,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- name: "Cache pre-commit"
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
@@ -720,7 +720,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -857,7 +857,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
with:
tool: cargo-codspeed

View File

@@ -34,11 +34,11 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build ruff
# A debug build means the script runs slower once it gets started,

View File

@@ -36,7 +36,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: Build Red Knot
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release

View File

@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
@@ -45,13 +45,15 @@ jobs:
- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v4"
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5"
- name: Run mypy_primer
shell: bash
run: |
cd ruff
PRIMER_SELECTOR="$(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt)"
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
@@ -62,13 +64,14 @@ jobs:
cd ..
echo "Project selector: $PRIMER_SELECTOR"
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx mypy_primer \
--repo ruff \
--type-checker knot \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy|werkzeug|bidict|async-utils)$' \
--project-selector "/($PRIMER_SELECTOR)\$" \
--output concise \
--debug > mypy_primer.diff || [ $? -eq 1 ]

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
pattern: wheels-*

View File

@@ -79,7 +79,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.4
rev: v0.11.6
hooks:
- id: ruff-format
- id: ruff
@@ -97,12 +97,12 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.5.2
rev: v1.6.0
hooks:
- id: zizmor
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.32.1
rev: 0.33.0
hooks:
- id: check-github-workflows

View File

@@ -1,5 +1,16 @@
# Changelog
## 0.11.6
### Preview features
- Avoid adding whitespace to the end of a docstring after an escaped quote ([#17216](https://github.com/astral-sh/ruff/pull/17216))
- \[`airflow`\] Extract `AIR311` from `AIR301` rules (`AIR301`, `AIR311`) ([#17310](https://github.com/astral-sh/ruff/pull/17310), [#17422](https://github.com/astral-sh/ruff/pull/17422))
### Bug fixes
- Raise syntax error when `\` is at end of file ([#17409](https://github.com/astral-sh/ruff/pull/17409))
## 0.11.5
### Preview features

88
Cargo.lock generated
View File

@@ -128,9 +128,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.97"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "argfile"
@@ -216,9 +216,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.11.3"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.9",
@@ -334,9 +334,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.35"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
"clap_derive",
@@ -344,9 +344,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.35"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -487,7 +487,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1553,28 +1553,45 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.4"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
checksum = "59ec30f7142be6fe14e1b021f50b85db8df2d4324ea6e91ec3e5dcde092021d0"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.59.0",
]
[[package]]
name = "jiff-static"
version = "0.2.4"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
checksum = "526b834d727fd59d37b076b0c3236d9adde1b1729a4361e20b2026f738cc1dbe"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jobserver"
version = "0.1.32"
@@ -1628,9 +1645,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libcst"
@@ -1659,9 +1676,9 @@ dependencies = [
[[package]]
name = "libmimalloc-sys"
version = "0.1.41"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
dependencies = [
"cc",
"libc",
@@ -1797,9 +1814,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mimalloc"
version = "0.1.45"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
dependencies = [
"libmimalloc-sys",
]
@@ -2310,9 +2327,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
@@ -2403,13 +2420,12 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy",
]
[[package]]
@@ -2476,7 +2492,6 @@ version = "0.0.0"
dependencies = [
"anyhow",
"argfile",
"chrono",
"clap",
"colored 3.0.0",
"countme",
@@ -2485,6 +2500,7 @@ dependencies = [
"filetime",
"insta",
"insta-cmd",
"jiff",
"rayon",
"red_knot_project",
"red_knot_python_semantic",
@@ -2756,7 +2772,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.5"
version = "0.11.6"
dependencies = [
"anyhow",
"argfile",
@@ -2764,7 +2780,6 @@ dependencies = [
"bincode",
"bitflags 2.9.0",
"cachedir",
"chrono",
"clap",
"clap_complete_command",
"clearscreen",
@@ -2777,6 +2792,7 @@ dependencies = [
"insta-cmd",
"is-macro",
"itertools 0.14.0",
"jiff",
"log",
"mimalloc",
"notify",
@@ -2991,12 +3007,11 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.5"
version = "0.11.6"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.9.0",
"chrono",
"clap",
"colored 3.0.0",
"fern",
@@ -3007,6 +3022,7 @@ dependencies = [
"is-macro",
"is-wsl",
"itertools 0.14.0",
"jiff",
"libcst",
"log",
"memchr",
@@ -3067,7 +3083,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
"rand 0.9.0",
"rand 0.9.1",
"ruff_diagnostics",
"ruff_source_file",
"ruff_text_size",
@@ -3317,7 +3333,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.5"
version = "0.11.6"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3658,9 +3674,9 @@ dependencies = [
[[package]]
name = "shellexpand"
version = "3.1.0"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
dependencies = [
"dirs",
]
@@ -4311,7 +4327,7 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
"js-sys",
"rand 0.9.0",
"rand 0.9.1",
"uuid-macro-internal",
"wasm-bindgen",
]
@@ -4582,7 +4598,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]

View File

@@ -55,7 +55,6 @@ bitflags = { version = "2.5.0" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
camino = { version = "1.1.7" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "4.0.0" }
@@ -95,6 +94,7 @@ insta-cmd = { version = "0.6.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.14.0" }
jiff = { version = "0.2.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "0.1.2" }
libc = { version = "0.2.153" }

View File

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

View File

@@ -20,12 +20,12 @@ ruff_python_ast = { workspace = true }
anyhow = { workspace = true }
argfile = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }
colored = { workspace = true }
countme = { workspace = true, features = ["enable"] }
crossbeam = { workspace = true }
ctrlc = { version = "3.4.4" }
jiff = { workspace = true }
rayon = { workspace = true }
salsa = { workspace = true }
tracing = { workspace = true, features = ["release_max_level_debug"] }

View File

@@ -31,7 +31,7 @@ mypy_primer \
```
This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the
diff for all projects, you currently need to copy the project-selector regex from the CI pipeline in `.github/workflows/mypy_primer.yaml`.
diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt))\$"`.
You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration
option to work correctly.

View File

@@ -190,8 +190,8 @@ where
let ansi = writer.has_ansi_escapes();
if self.display_timestamp {
let timestamp = chrono::Local::now()
.format("%Y-%m-%d %H:%M:%S.%f")
let timestamp = jiff::Zoned::now()
.strftime("%Y-%m-%d %H:%M:%S.%f")
.to_string();
if ansi {
write!(writer, "{} ", timestamp.dimmed())?;
@@ -199,7 +199,7 @@ where
write!(
writer,
"{} ",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S.%f")
jiff::Zoned::now().strftime("%Y-%m-%d %H:%M:%S.%f")
)?;
}
}

View File

@@ -83,13 +83,13 @@ fn config_override_python_platform() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
info: revealed-type
info: revealed-type: Revealed type
--> <temp_dir>/test.py:5:1
|
3 | from typing_extensions import reveal_type
4 |
5 | reveal_type(sys.platform)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ Revealed type is `Literal["linux"]`
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal["linux"]`
|
Found 1 diagnostic
@@ -101,13 +101,13 @@ fn config_override_python_platform() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
info: revealed-type
info: revealed-type: Revealed type
--> <temp_dir>/test.py:5:1
|
3 | from typing_extensions import reveal_type
4 |
5 | reveal_type(sys.platform)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ Revealed type is `LiteralString`
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `LiteralString`
|
Found 1 diagnostic
@@ -252,7 +252,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
r#"
y = 4 / 0
for a in range(0, y):
for a in range(0, int(y)):
x = a
print(x) # possibly-unresolved-reference
@@ -271,7 +271,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
@@ -307,7 +307,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
4 | for a in range(0, int(y)):
|
Found 1 diagnostic
@@ -328,7 +328,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
y = 4 / 0
for a in range(0, y):
for a in range(0, int(y)):
x = a
print(x) # possibly-unresolved-reference
@@ -358,7 +358,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
5 |
6 | for a in range(0, y):
6 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
@@ -405,7 +405,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
5 |
6 | for a in range(0, y):
6 | for a in range(0, int(y)):
|
Found 2 diagnostics
@@ -426,7 +426,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
r#"
y = 4 / 0
for a in range(0, y):
for a in range(0, int(y)):
x = a
print(x) # possibly-unresolved-reference
@@ -445,7 +445,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
@@ -482,7 +482,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
4 | for a in range(0, int(y)):
|
Found 1 diagnostic
@@ -584,12 +584,12 @@ fn exit_code_only_info() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
info: revealed-type
info: revealed-type: Revealed type
--> <temp_dir>/test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| ^^^^^^^^^^^^^^ Revealed type is `Literal[1]`
| ^^^^^^^^^^^^^^ `Literal[1]`
|
Found 1 diagnostic
@@ -614,12 +614,12 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
info: revealed-type
info: revealed-type: Revealed type
--> <temp_dir>/test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| ^^^^^^^^^^^^^^ Revealed type is `Literal[1]`
| ^^^^^^^^^^^^^^ `Literal[1]`
|
Found 1 diagnostic
@@ -814,7 +814,7 @@ fn user_configuration() -> anyhow::Result<()> {
r#"
y = 4 / 0
for a in range(0, y):
for a in range(0, int(y)):
x = a
print(x)
@@ -841,7 +841,7 @@ fn user_configuration() -> anyhow::Result<()> {
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
@@ -883,7 +883,7 @@ fn user_configuration() -> anyhow::Result<()> {
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
4 | for a in range(0, int(y)):
|
error: lint:possibly-unresolved-reference
@@ -1052,6 +1052,39 @@ fn concise_diagnostics() -> anyhow::Result<()> {
Ok(())
}
/// This tests the diagnostic format for revealed type.
///
/// This test was introduced because changes were made to
/// how the revealed type diagnostic was constructed and
/// formatted in "verbose" mode. But it required extra
/// logic to ensure the concise version didn't regress on
/// information content. So this test was introduced to
/// capture that.
#[test]
fn concise_revealed_type() -> anyhow::Result<()> {
let case = TestCase::with_file(
"test.py",
r#"
from typing_extensions import reveal_type
x = "hello"
reveal_type(x)
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#"
success: true
exit_code: 0
----- stdout -----
info[revealed-type] <temp_dir>/test.py:5:1: Revealed type: `Literal["hello"]`
Found 1 diagnostic
----- stderr -----
"#);
Ok(())
}
struct TestCase {
_temp_dir: TempDir,
_settings_scope: SettingsBindDropGuard,

View File

@@ -10,7 +10,7 @@ pub(crate) mod tests {
use super::Db;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb};
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
@@ -83,6 +83,10 @@ pub(crate) mod tests {
fn files(&self) -> &Files {
&self.files
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
}
impl Upcast<dyn SourceDb> for TestDb {

View File

@@ -421,16 +421,16 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r#"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
--> stdlib/builtins.pyi:438:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
437 |
438 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
439 | @overload
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:13
@@ -440,7 +440,7 @@ mod tests {
4 | a
| ^
|
"###);
"#);
}
#[test]
fn goto_type_of_expression_with_literal_node() {
@@ -450,16 +450,16 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r#"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
--> stdlib/builtins.pyi:438:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
437 |
438 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
439 | @overload
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:2:22
@@ -467,7 +467,7 @@ mod tests {
2 | a: str = "test"
| ^^^^^^
|
"###);
"#);
}
#[test]
@@ -532,16 +532,16 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r#"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
--> stdlib/builtins.pyi:438:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
437 |
438 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
439 | @overload
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:18
@@ -551,7 +551,7 @@ mod tests {
4 | test(a= "123")
| ^
|
"###);
"#);
}
#[test]
@@ -567,16 +567,16 @@ mod tests {
// TODO: This should jump to `str` and not `int` because
// the keyword is typed as a string. It's only the passed argument that
// is an int. Navigating to `str` would match pyright's behavior.
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:234:7
--> stdlib/builtins.pyi:231:7
|
232 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
233 |
234 | class int:
229 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
230 |
231 | class int:
| ^^^
235 | @overload
236 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...
232 | @overload
233 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...
|
info: Source
--> /main.py:4:18
@@ -586,7 +586,7 @@ mod tests {
4 | test(a= 123)
| ^
|
"###);
");
}
#[test]
@@ -601,16 +601,16 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r#"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:1098:7
--> stdlib/builtins.pyi:1086:7
|
1096 | def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
1097 |
1098 | class dict(MutableMapping[_KT, _VT]):
1084 | def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
1085 |
1086 | class dict(MutableMapping[_KT, _VT]):
| ^^^^
1099 | # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics
1100 | # Also multiprocessing.managers.SyncManager.dict()
1087 | # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics
1088 | # Also multiprocessing.managers.SyncManager.dict()
|
info: Source
--> /main.py:6:5
@@ -620,7 +620,7 @@ f(**kwargs<CURSOR>)
6 | f(**kwargs)
| ^^^^^^
|
"###);
"#);
}
#[test]
@@ -632,16 +632,16 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
--> stdlib/builtins.pyi:438:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
437 |
438 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
439 | @overload
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:3:17
@@ -650,7 +650,7 @@ f(**kwargs<CURSOR>)
3 | a
| ^
|
"###);
");
}
#[test]
@@ -725,16 +725,16 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
--> stdlib/builtins.pyi:438:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
437 |
438 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
439 | @overload
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:27
@@ -744,7 +744,7 @@ f(**kwargs<CURSOR>)
4 | print(a)
| ^
|
"###);
");
}
#[test]
@@ -758,13 +758,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> stdlib/types.pyi:677:11
--> stdlib/types.pyi:671:11
|
675 | if sys.version_info >= (3, 10):
676 | @final
677 | class NoneType:
669 | if sys.version_info >= (3, 10):
670 | @final
671 | class NoneType:
| ^^^^^^^^
678 | def __bool__(self) -> Literal[False]: ...
672 | def __bool__(self) -> Literal[False]: ...
|
info: Source
--> /main.py:3:17
@@ -775,14 +775,14 @@ f(**kwargs<CURSOR>)
|
info: lint:goto-type-definition: Type definition
--> stdlib/builtins.pyi:443:7
--> stdlib/builtins.pyi:438:7
|
441 | def __getitem__(self, key: int, /) -> str | int | None: ...
442 |
443 | class str(Sequence[str]):
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
437 |
438 | class str(Sequence[str]):
| ^^^
444 | @overload
445 | def __new__(cls, object: object = ...) -> Self: ...
439 | @overload
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:3:17

View File

@@ -214,11 +214,11 @@ mod tests {
"#,
);
assert_snapshot!(test.hover(), @r"
Literal[foo]
assert_snapshot!(test.hover(), @r###"
def foo(a, b) -> Unknown
---------------------------------------------
```text
Literal[foo]
def foo(a, b) -> Unknown
```
---------------------------------------------
info: lint:hover: Hovered content is
@@ -231,7 +231,7 @@ mod tests {
| |
| source
|
");
"###);
}
#[test]
@@ -312,11 +312,11 @@ mod tests {
"#,
);
assert_snapshot!(test.hover(), @r"
Literal[foo, bar]
assert_snapshot!(test.hover(), @r###"
(def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown)
---------------------------------------------
```text
Literal[foo, bar]
(def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown)
```
---------------------------------------------
info: lint:hover: Hovered content is
@@ -329,7 +329,7 @@ mod tests {
| |
| source
|
");
"###);
}
#[test]

View File

@@ -0,0 +1,15 @@
# Regression test for https://github.com/astral-sh/ruff/issues/17215
# panicked in commit 1a6a10b30
# error message:
# dependency graph cycle querying all_narrowing_constraints_for_expression(Id(8591))
def f(a: A, b: B, c: C):
unknown_a: UA = make_unknown()
unknown_b: UB = make_unknown()
unknown_c: UC = make_unknown()
unknown_d: UD = make_unknown()
if unknown_a and unknown_b:
if unknown_c:
if unknown_d:
return a, b, c

View File

@@ -0,0 +1,22 @@
# Regression test for https://github.com/astral-sh/ruff/issues/17215
# panicked in commit 1a6a10b30
# error message:
# dependency graph cycle querying all_negative_narrowing_constraints_for_expression(Id(859f))
def f(f1: bool, f2: bool, f3: bool, f4: bool):
o1: UnknownClass = make_o()
o2: UnknownClass = make_o()
o3: UnknownClass = make_o()
o4: UnknownClass = make_o()
if f1 and f2 and f3 and f4:
if o1 == o2:
return None
if o2 == o3:
return None
if o3 == o4:
return None
if o4 == o1:
return None
return o1, o2, o3, o4

View File

@@ -149,6 +149,10 @@ impl SourceDb for ProjectDatabase {
fn files(&self) -> &Files {
&self.files
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
}
#[salsa::db]
@@ -207,7 +211,7 @@ pub(crate) mod tests {
use salsa::Event;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::Db as SemanticDb;
use red_knot_python_semantic::{Db as SemanticDb, Program};
use ruff_db::files::Files;
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
@@ -281,6 +285,10 @@ pub(crate) mod tests {
fn files(&self) -> &Files {
&self.files
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
Program::get(self).python_version(self)
}
}
impl Upcast<dyn SemanticDb> for TestDb {

View File

@@ -10,7 +10,8 @@ use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSele
use red_knot_python_semantic::register_lints;
use red_knot_python_semantic::types::check_types;
use ruff_db::diagnostic::{
create_parse_diagnostic, Annotation, Diagnostic, DiagnosticId, Severity, Span,
create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic,
DiagnosticId, Severity, Span,
};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
@@ -424,6 +425,13 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
.map(|error| create_parse_diagnostic(file, error)),
);
diagnostics.extend(
parsed
.unsupported_syntax_errors()
.iter()
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
);
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
diagnostics.sort_unstable_by_key(|diagnostic| {
@@ -520,11 +528,13 @@ mod tests {
use crate::db::tests::TestDb;
use crate::{check_file_impl, ProjectMetadata};
use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
use ruff_db::files::system_path_to_file;
use ruff_db::source::source_text;
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::name::Name;
use ruff_python_ast::PythonVersion;
#[test]
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
@@ -532,6 +542,16 @@ mod tests {
let mut db = TestDb::new(project);
let path = SystemPath::new("test.py");
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
},
)
.expect("Failed to configure program settings");
db.write_file(path, "x = 10")?;
let file = system_path_to_file(&db, path).unwrap();

View File

@@ -6,7 +6,9 @@ use ruff_db::parsed::parsed_module;
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
use ruff_python_ast::visitor::source_order;
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
use ruff_python_ast::{self as ast, Alias, Expr, Parameter, ParameterWithDefault, Stmt};
use ruff_python_ast::{
self as ast, Alias, Comprehension, Expr, Parameter, ParameterWithDefault, Stmt,
};
fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result<ProjectDatabase> {
let project = ProjectMetadata::discover(project_root, &system)?;
@@ -258,6 +260,14 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
source_order::walk_expr(self, expr);
}
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
self.visit_expr(&comprehension.iter);
self.visit_target(&comprehension.target);
for if_expr in &comprehension.ifs {
self.visit_expr(if_expr);
}
}
fn visit_parameter(&mut self, parameter: &Parameter) {
let _ty = parameter.inferred_type(&self.model);

View File

@@ -237,6 +237,11 @@ def _(c: Callable[[Concatenate[int, str, ...], int], int]):
## Using `typing.ParamSpec`
```toml
[environment]
python-version = "3.12"
```
Using a `ParamSpec` in a `Callable` annotation:
```py
@@ -289,7 +294,7 @@ def _(c: Callable[[int, Unpack[Ts]], int]):
from typing import Callable
def _(c: Callable[[int], int]):
reveal_type(c.__init__) # revealed: Literal[__init__]
reveal_type(c.__init__) # revealed: def __init__(self) -> None
reveal_type(c.__class__) # revealed: type
# TODO: The member lookup for `Callable` uses `object` which does not have a `__call__`

View File

@@ -48,6 +48,11 @@ reveal_type(get_foo()) # revealed: Foo
## Deferred self-reference annotations in a class definition
```toml
[environment]
python-version = "3.12"
```
```py
from __future__ import annotations
@@ -94,6 +99,11 @@ class Foo:
## Non-deferred self-reference annotations in a class definition
```toml
[environment]
python-version = "3.12"
```
```py
class Foo:
# error: [unresolved-reference]
@@ -146,3 +156,24 @@ def _():
def f(self) -> C:
return self
```
## Base class references
### Not deferred by __future__.annotations
```py
from __future__ import annotations
class A(B): # error: [unresolved-reference]
pass
class B:
pass
```
### Deferred in stub files
```pyi
class A(B): ...
class B: ...
```

View File

@@ -89,7 +89,7 @@ def _(
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(generics)
reveal_type(r) # revealed: @Todo(unknown type subscript)
```
## Invalid Collection based AST nodes

View File

@@ -68,7 +68,7 @@ def x(
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
):
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(a1) # revealed: Literal[1, 2, 3, 5, "foo"] | None
reveal_type(a2) # revealed: Literal["w", "r"]
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
@@ -108,7 +108,7 @@ def union_example(
None,
],
):
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
reveal_type(x) # revealed: Unknown | Literal[-1, 0, 1, "A", "B", "foo", "bar", b"A", b"\x00", b"\x07", True] | None
```
## Detecting Literal outside typing and typing_extensions
@@ -137,7 +137,7 @@ from other import Literal
a1: Literal[26]
def f():
reveal_type(a1) # revealed: @Todo(generics)
reveal_type(a1) # revealed: @Todo(unknown type subscript)
```
## Detecting typing_extensions.Literal

View File

@@ -72,13 +72,11 @@ reveal_type(baz) # revealed: Literal["bazfoo"]
qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(return type of overloaded function)
reveal_type(foo.join(qux)) # revealed: LiteralString
template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
# TODO: Infer `LiteralString`
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of overloaded function)
reveal_type(template.format(foo, bar)) # revealed: LiteralString
```
### Assignability

View File

@@ -12,7 +12,7 @@ X = GenericAlias(type, ())
A = NewType("A", int)
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
# to be compatible with `type`
# error: [invalid-argument-type] "Object of type `NewType` cannot be assigned to parameter 2 (`origin`) of function `__new__`; expected type `type`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `type`, found `NewType`"
B = GenericAlias(A, ())
def _(

View File

@@ -1,5 +1,10 @@
# Starred expression annotations
```toml
[environment]
python-version = "3.11"
```
Type annotations for `*args` can be starred expressions themselves:
```py

View File

@@ -67,21 +67,24 @@ import typing
####################
### Built-ins
####################
class ListSubclass(typing.List): ...
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
# TODO: generic protocols
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]]
# TODO: generic protocols
# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
# TODO: generic protocols
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
@@ -92,11 +95,12 @@ reveal_type(FrozenSetSubclass.__mro__)
####################
### `collections`
####################
class ChainMapSubclass(typing.ChainMap): ...
# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]]
# TODO: generic protocols
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
@@ -113,7 +117,8 @@ reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
# TODO: generic protocols
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...

View File

@@ -105,7 +105,7 @@ def f1(
from typing import Literal
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"]
```
## Class variables

View File

@@ -41,7 +41,7 @@ class Foo:
One thing that is supported is error messages for using special forms in type expressions.
```py
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic
def _(
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression"
@@ -49,6 +49,7 @@ def _(
c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
e: ParamSpec,
f: Generic, # error: [invalid-type-form] "`typing.Generic` is not allowed in type expressions"
) -> None:
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -65,7 +66,7 @@ You can't inherit from most of these. `typing.Callable` is an exception.
```py
from typing import Callable
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic
class A(Self): ... # error: [invalid-base]
class B(Unpack): ... # error: [invalid-base]
@@ -73,12 +74,18 @@ class C(TypeGuard): ... # error: [invalid-base]
class D(TypeIs): ... # error: [invalid-base]
class E(Concatenate): ... # error: [invalid-base]
class F(Callable): ...
class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`"
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
```
## Subscriptability
```toml
[environment]
python-version = "3.12"
```
Some of these are not subscriptable:
```py

View File

@@ -25,6 +25,11 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
## Tuple annotations are understood
```toml
[environment]
python-version = "3.12"
```
`module.py`:
```py
@@ -56,7 +61,7 @@ reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
reveal_type(e) # revealed: @Todo(full tuple[...] support)
reveal_type(f) # revealed: @Todo(full tuple[...] support)
reveal_type(g) # revealed: @Todo(full tuple[...] support)
reveal_type(h) # revealed: tuple[@Todo(generics), @Todo(generics)]
reveal_type(h) # revealed: tuple[@Todo(specialized non-generic class), @Todo(specialized non-generic class)]
reveal_type(i) # revealed: tuple[str | int, str | int]
reveal_type(j) # revealed: tuple[str | int]

View File

@@ -41,8 +41,7 @@ reveal_type(c_instance.declared_only) # revealed: bytes
reveal_type(c_instance.declared_and_bound) # revealed: bool
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound.
# mypy and pyright do not show an error here.
# error: [possibly-unbound-attribute]
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`.
@@ -303,7 +302,7 @@ class C:
c_instance = C()
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking)
reveal_type(c_instance.b) # revealed: Unknown
```
#### Attributes defined in for-loop (unpacking)
@@ -339,8 +338,10 @@ class C:
for self.z in NonIterable():
pass
# Iterable might be empty
# error: [possibly-unbound-attribute]
reveal_type(C().x) # revealed: Unknown | int
# error: [possibly-unbound-attribute]
reveal_type(C().y) # revealed: Unknown | str
```
@@ -396,21 +397,33 @@ class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
class TupleIterator:
def __next__(self) -> tuple[int, str]:
return (1, "a")
class TupleIterable:
def __iter__(self) -> TupleIterator:
return TupleIterator()
class C:
def __init__(self) -> None:
[... for self.a in IntIterable()]
[... for (self.b, self.c) in TupleIterable()]
[... for self.d in IntIterable() for self.e in IntIterable()]
c_instance = C()
# TODO: Should be `Unknown | int`
# error: [unresolved-attribute]
reveal_type(c_instance.a) # revealed: Unknown
reveal_type(c_instance.a) # revealed: Unknown | int
reveal_type(c_instance.b) # revealed: Unknown | int
reveal_type(c_instance.c) # revealed: Unknown | str
reveal_type(c_instance.d) # revealed: Unknown | int
reveal_type(c_instance.e) # revealed: Unknown | int
```
#### Conditionally declared / bound attributes
We currently do not raise a diagnostic or change behavior if an attribute is only conditionally
defined. This is consistent with what mypy and pyright do.
Attributes are possibly unbound if they, or the method to which they are added are conditionally
declared / bound.
```py
def flag() -> bool:
@@ -428,9 +441,13 @@ class C:
c_instance = C()
# error: [possibly-unbound-attribute]
reveal_type(c_instance.a1) # revealed: str | None
# error: [possibly-unbound-attribute]
reveal_type(c_instance.a2) # revealed: str | None
# error: [possibly-unbound-attribute]
reveal_type(c_instance.b1) # revealed: Unknown | Literal[1]
# error: [possibly-unbound-attribute]
reveal_type(c_instance.b2) # revealed: Unknown | Literal[1]
```
@@ -539,10 +556,88 @@ class C:
if (2 + 3) < 4:
self.x: str = "a"
# TODO: Ideally, this would result in a `unresolved-attribute` error. But mypy and pyright
# do not support this either (for conditions that can only be resolved to `False` in type
# inference), so it does not seem to be particularly important.
reveal_type(C().x) # revealed: str
# error: [unresolved-attribute]
reveal_type(C().x) # revealed: Unknown
```
```py
class C:
def __init__(self, cond: bool) -> None:
if True:
self.a = 1
else:
self.a = "a"
if False:
self.b = 2
if cond:
return
self.c = 3
self.d = 4
self.d = 5
def set_c(self, c: str) -> None:
self.c = c
if False:
def set_e(self, e: str) -> None:
self.e = e
reveal_type(C(True).a) # revealed: Unknown | Literal[1]
# error: [unresolved-attribute]
reveal_type(C(True).b) # revealed: Unknown
reveal_type(C(True).c) # revealed: Unknown | Literal[3] | str
# TODO: this attribute is possibly unbound
reveal_type(C(True).d) # revealed: Unknown | Literal[5]
# error: [unresolved-attribute]
reveal_type(C(True).e) # revealed: Unknown
```
#### Attributes considered always bound
```py
class C:
def __init__(self, cond: bool):
self.x = 1
if cond:
raise ValueError("Something went wrong")
# We consider this attribute is always bound.
# This is because, it is not possible to access a partially-initialized object by normal means.
self.y = 2
reveal_type(C(False).x) # revealed: Unknown | Literal[1]
reveal_type(C(False).y) # revealed: Unknown | Literal[2]
class C:
def __init__(self, b: bytes) -> None:
self.b = b
try:
s = b.decode()
except UnicodeDecodeError:
raise ValueError("Invalid UTF-8 sequence")
self.s = s
reveal_type(C(b"abc").b) # revealed: Unknown | bytes
reveal_type(C(b"abc").s) # revealed: Unknown | str
class C:
def __init__(self, iter) -> None:
self.x = 1
for _ in iter:
pass
# The for-loop may not stop,
# but we consider the subsequent attributes to be definitely-bound.
self.y = 2
reveal_type(C([]).x) # revealed: Unknown | Literal[1]
reveal_type(C([]).y) # revealed: Unknown | Literal[2]
```
#### Diagnostics are reported for the right-hand side of attribute assignments
@@ -1046,13 +1141,18 @@ def _(flag: bool):
def __init(self):
if flag:
self.x = 1
self.y = "a"
else:
self.y = "b"
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
# if we ever will (or want to)
# error: [possibly-unbound-attribute]
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
# Same here
# error: [possibly-unbound-attribute]
Foo().x = 2
reveal_type(Foo().y) # revealed: Unknown | Literal["a", "b"]
Foo().y = "c"
```
### Unions with all paths unbound
@@ -1285,7 +1385,7 @@ from typing import Any
class Foo(Any): ...
reveal_type(Foo.bar) # revealed: Any
reveal_type(Foo.__repr__) # revealed: Literal[__repr__] & Any
reveal_type(Foo.__repr__) # revealed: (def __repr__(self) -> str) & Any
```
Similar principles apply if `Any` appears in the middle of an inheritance hierarchy:
@@ -1577,14 +1677,14 @@ functions are instances of that class:
def f(): ...
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
```
Some attributes are special-cased, however:
```py
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(f.__call__) # revealed: <bound method `__call__` of `Literal[f]`>
reveal_type(f.__call__) # revealed: <method-wrapper `__call__` of `f`>
```
### Int-literal attributes
@@ -1593,7 +1693,7 @@ Most attribute accesses on int-literal types are delegated to `builtins.int`, si
integers are instances of that class:
```py
reveal_type((2).bit_length) # revealed: <bound method `bit_length` of `Literal[2]`>
reveal_type((2).bit_length) # revealed: bound method Literal[2].bit_length() -> int
reveal_type((2).denominator) # revealed: Literal[1]
```
@@ -1610,8 +1710,10 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
bools are instances of that class:
```py
reveal_type(True.__and__) # revealed: <bound method `__and__` of `Literal[True]`>
reveal_type(False.__or__) # revealed: <bound method `__or__` of `Literal[False]`>
# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int]
reveal_type(True.__and__)
# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int]
reveal_type(False.__or__)
```
Some attributes are special-cased, however:
@@ -1626,8 +1728,10 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
```py
reveal_type(b"foo".join) # revealed: <bound method `join` of `Literal[b"foo"]`>
reveal_type(b"foo".endswith) # revealed: <bound method `endswith` of `Literal[b"foo"]`>
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(specialized non-generic class), /) -> bytes
reveal_type(b"foo".join)
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
reveal_type(b"foo".endswith)
```
## Instance attribute edge cases
@@ -1728,6 +1832,89 @@ def f(never: Never):
never.another_attribute = never
```
### Cyclic implicit attributes
Inferring types for undeclared implicit attributes can be cyclic:
```py
class C:
def __init__(self):
self.x = 1
def copy(self, other: "C"):
self.x = other.x
reveal_type(C().x) # revealed: Unknown | Literal[1]
```
If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute:
```py
class D:
def copy(self, other: "D"):
self.x = other.x
reveal_type(D().x) # revealed: Unknown
```
If there is an annotation for a name, we don't try to infer any type from the RHS of assignments to
that name, so these cases don't trigger any cycle:
```py
class E:
def __init__(self):
self.x: int = 1
def copy(self, other: "E"):
self.x = other.x
reveal_type(E().x) # revealed: int
class F:
def __init__(self):
self.x = 1
def copy(self, other: "F"):
self.x: int = other.x
reveal_type(F().x) # revealed: int
class G:
def copy(self, other: "G"):
self.x: int = other.x
reveal_type(G().x) # revealed: int
```
We can even handle cycles involving multiple classes:
```py
class A:
def __init__(self):
self.x = 1
def copy(self, other: "B"):
self.x = other.x
class B:
def copy(self, other: "A"):
self.x = other.x
reveal_type(B().x) # revealed: Unknown | Literal[1]
reveal_type(A().x) # revealed: Unknown | Literal[1]
```
This case additionally tests our union/intersection simplification logic:
```py
class H:
def __init__(self):
self.x = 1
def copy(self, other: "H"):
self.x = other.x or self.x
```
### Builtin types attributes
This test can probably be removed eventually, but we currently include it because we do not yet
@@ -1779,20 +1966,6 @@ reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
```
## `super()`
`super()` is not supported yet, but we do not emit false positives on `super()` calls.
```py
class Foo:
def bar(self) -> int:
return 42
class Bar(Foo):
def bar(self) -> int:
return super().bar()
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View File

@@ -350,30 +350,30 @@ reveal_type(no() + no()) # revealed: Unknown
def f():
pass
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f + f) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f - f) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f * f) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f @ f) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f / f) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f % f) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f**f) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f << f) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f >> f) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f | f) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f ^ f) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f & f) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
reveal_type(f // f) # revealed: Unknown
```

View File

@@ -310,9 +310,7 @@ reveal_type(A() + 1) # revealed: A
reveal_type(1 + A()) # revealed: A
reveal_type(A() + "foo") # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type("foo" + A()) # revealed: @Todo(return type of overloaded function)
reveal_type("foo" + A()) # revealed: A
reveal_type(A() + b"foo") # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
@@ -320,16 +318,14 @@ reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(A() + ()) # revealed: A
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
reveal_type(() + A()) # revealed: @Todo(return type of overloaded function)
reveal_type(() + A()) # revealed: @Todo(full tuple[...] support)
literal_string_instance = "foo" * 1_000_000_000
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
reveal_type(literal_string_instance) # revealed: LiteralString
reveal_type(A() + literal_string_instance) # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of overloaded function)
reveal_type(literal_string_instance + A()) # revealed: A
```
## Operations involving instances of classes inheriting from `Any`

View File

@@ -50,9 +50,11 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int
reveal_type(2**largest_u32) # revealed: int
def variable(x: int):
reveal_type(x**2) # revealed: @Todo(return type of overloaded function)
reveal_type(2**x) # revealed: @Todo(return type of overloaded function)
reveal_type(x**x) # revealed: @Todo(return type of overloaded function)
reveal_type(x**2) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(2**x) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(x**x) # revealed: int
```
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but

View File

@@ -43,7 +43,7 @@ if True and (x := 1):
```py
def _(flag: bool):
flag or (x := 1) or reveal_type(x) # revealed: Literal[1]
flag or (x := 1) or reveal_type(x) # revealed: Never
# error: [unresolved-reference]
flag or reveal_type(y) or (y := 1) # revealed: Unknown

View File

@@ -9,8 +9,8 @@ def _(c: Callable[[], int]):
def _(c: Callable[[int, str], int]):
reveal_type(c(1, "a")) # revealed: int
# error: [invalid-argument-type] "Object of type `Literal["a"]` cannot be assigned to parameter 1; expected type `int`"
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2; expected type `str`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["a"]`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
reveal_type(c("a", 1)) # revealed: int
```

View File

@@ -85,7 +85,7 @@ class C:
c = C()
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(c("foo")) # revealed: int
```
@@ -99,7 +99,7 @@ class C:
c = C()
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of bound method `__call__`; expected type `int`"
# error: 13 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `C`"
reveal_type(c()) # revealed: int
```

View File

@@ -98,7 +98,7 @@ def _(flag: bool) -> None:
def __new__(cls, x: int, y: int = 1): ...
reveal_type(Foo(1)) # revealed: Foo
# error: [invalid-argument-type] "Object of type `Literal["1"]` cannot be assigned to parameter 2 (`x`) of function `__new__`; expected type `int`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["1"]`"
reveal_type(Foo("1")) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
reveal_type(Foo()) # revealed: Foo
@@ -210,7 +210,7 @@ def _(flag: bool) -> None:
def __init__(self, x: int, y: int = 1): ...
reveal_type(Foo(1)) # revealed: Foo
# error: [invalid-argument-type] "Object of type `Literal["1"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `int`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["1"]`"
reveal_type(Foo("1")) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(Foo()) # revealed: Foo

View File

@@ -21,6 +21,11 @@ reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
## Generic
```toml
[environment]
python-version = "3.12"
```
```py
def get_int[T]() -> int:
return 42
@@ -72,7 +77,7 @@ def _(flag: bool):
def f(x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo")) # revealed: int
```
@@ -82,7 +87,7 @@ reveal_type(f("foo")) # revealed: int
def f(x: int, /) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo")) # revealed: int
```
@@ -92,7 +97,7 @@ reveal_type(f("foo")) # revealed: int
def f(*args: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `*args` of function `f`; expected type `int`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo")) # revealed: int
```
@@ -102,7 +107,7 @@ reveal_type(f("foo")) # revealed: int
def f(x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo")) # revealed: int
```
@@ -112,7 +117,7 @@ reveal_type(f(x="foo")) # revealed: int
def f(*, x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo")) # revealed: int
```
@@ -122,7 +127,7 @@ reveal_type(f(x="foo")) # revealed: int
def f(**kwargs: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `**kwargs` of function `f`; expected type `int`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo")) # revealed: int
```
@@ -132,8 +137,8 @@ reveal_type(f(x="foo")) # revealed: int
def f(x: int = 1, y: str = "foo") -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal[2]` cannot be assigned to parameter `y` of function `f`; expected type `str`"
# error: 20 [invalid-argument-type] "Object of type `Literal["bar"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[2]`"
# error: 20 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["bar"]`"
reveal_type(f(y=2, x="bar")) # revealed: int
```

View File

@@ -57,7 +57,8 @@ We can access attributes on objects of all kinds:
import sys
reveal_type(inspect.getattr_static(sys, "dont_write_bytecode")) # revealed: bool
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
# revealed: def getattr_static(obj: object, attr: str, default: Any | None = ellipsis) -> Any
reveal_type(inspect.getattr_static(inspect, "getattr_static"))
reveal_type(inspect.getattr_static(1, "real")) # revealed: property
```
@@ -114,7 +115,7 @@ inspect.getattr_static()
# error: [missing-argument] "No argument provided for required parameter `attr`"
inspect.getattr_static(C())
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`attr`) of function `getattr_static`; expected type `str`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
inspect.getattr_static(C(), 1)
# error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4"
@@ -143,8 +144,9 @@ from typing import Any
def _(a: Any, tuple_of_any: tuple[Any]):
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
# TODO: Ideally, this would just be `Literal[index]`
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) # revealed: Literal[index] | Literal["default"]
# TODO: Ideally, this would just be `def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int`
# revealed: (def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int) | Literal["default"]
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default"))
```
[official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static

View File

@@ -24,7 +24,7 @@ to the valid order:
def f(**kw: int, x: str) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `str`"
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
reveal_type(f(1)) # revealed: int
```
@@ -38,7 +38,7 @@ def f(x: int = 1, y: str) -> int:
return 1
reveal_type(f(y="foo")) # revealed: int
# error: [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
# error: [missing-argument] "No argument provided for required parameter `y` of function `f`"
reveal_type(f("foo")) # revealed: int
```

View File

@@ -32,20 +32,20 @@ the latter case, it returns a *bound method* object:
```py
from inspect import getattr_static
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
reveal_type(getattr_static(C, "f")) # revealed: def f(self, x: int) -> str
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: Literal[f]
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `C`>
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: def f(self, x: int) -> str
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method C.f(x: int) -> str
```
In conclusion, this is why we see the following two types when accessing the `f` attribute on the
class object `C` and on an instance `C()`:
```py
reveal_type(C.f) # revealed: Literal[f]
reveal_type(C().f) # revealed: <bound method `f` of `C`>
reveal_type(C.f) # revealed: def f(self, x: int) -> str
reveal_type(C().f) # revealed: bound method C.f(x: int) -> str
```
A bound method is a callable object that contains a reference to the `instance` that it was called
@@ -56,7 +56,7 @@ via `__func__`):
bound_method = C().f
reveal_type(bound_method.__self__) # revealed: C
reveal_type(bound_method.__func__) # revealed: Literal[f]
reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str
```
When we call the bound method, the `instance` is implicitly passed as the first argument (`self`):
@@ -80,13 +80,13 @@ When we access methods from derived classes, they will be bound to instances of
class D(C):
pass
reveal_type(D().f) # revealed: <bound method `f` of `D`>
reveal_type(D().f) # revealed: bound method D.f(x: int) -> str
```
If we access an attribute on a bound method object itself, it will defer to `types.MethodType`:
```py
reveal_type(bound_method.__hash__) # revealed: <bound method `__hash__` of `MethodType`>
reveal_type(bound_method.__hash__) # revealed: bound method MethodType.__hash__() -> int
```
If an attribute is not available on the bound method object, it will be looked up on the underlying
@@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd
methods, even though it is not available on `types.MethodType`:
```py
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
```
## Basic method calls on class objects and instances
@@ -181,10 +181,10 @@ class B:
return "a"
def f(a_or_b: A | B, any_or_a: Any | A):
reveal_type(a_or_b.f) # revealed: <bound method `f` of `A`> | <bound method `f` of `B`>
reveal_type(a_or_b.f) # revealed: (bound method A.f() -> int) | (bound method B.f() -> str)
reveal_type(a_or_b.f()) # revealed: int | str
reveal_type(any_or_a.f) # revealed: Any | <bound method `f` of `A`>
reveal_type(any_or_a.f) # revealed: Any | (bound method A.f() -> int)
reveal_type(any_or_a.f()) # revealed: Any | int
```
@@ -198,7 +198,7 @@ python-version = "3.12"
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: <bound method `__or__` of `typing.TypeAliasType`>
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm
```
## Error cases: Calling `__get__` for methods
@@ -270,7 +270,7 @@ class Meta(type):
class C(metaclass=Meta):
pass
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(C.f) # revealed: bound method Literal[C].f(arg: int) -> str
reveal_type(C.f(1)) # revealed: str
```
@@ -322,8 +322,8 @@ class C:
def f(cls: type[C], x: int) -> str:
return "a"
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(C().f) # revealed: <bound method `f` of `type[C]`>
reveal_type(C.f) # revealed: bound method Literal[C].f(x: int) -> str
reveal_type(C().f) # revealed: bound method type[C].f(x: int) -> str
```
The `cls` method argument is then implicitly passed as the first argument when calling the method:
@@ -350,7 +350,7 @@ class D:
# This function is wrongly annotated, it should be `type[D]` instead of `D`
pass
# error: [invalid-argument-type] "Object of type `Literal[D]` cannot be assigned to parameter 1 (`cls`) of bound method `f`; expected type `D`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `D`, found `Literal[D]`"
D.f()
```
@@ -360,8 +360,8 @@ When a class method is accessed on a derived class, it is bound to that derived
class Derived(C):
pass
reveal_type(Derived.f) # revealed: <bound method `f` of `Literal[Derived]`>
reveal_type(Derived().f) # revealed: <bound method `f` of `type[Derived]`>
reveal_type(Derived.f) # revealed: bound method Literal[Derived].f(x: int) -> str
reveal_type(Derived().f) # revealed: bound method type[Derived].f(x: int) -> str
reveal_type(Derived.f(1)) # revealed: str
reveal_type(Derived().f(1)) # revealed: str
@@ -379,26 +379,31 @@ class C:
@classmethod
def f(cls): ...
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
reveal_type(getattr_static(C, "f")) # revealed: def f(cls) -> Unknown
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
```
But we correctly model how the `classmethod` descriptor works:
```py
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: <bound method `f` of `type[C]`>
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: bound method Literal[C].f() -> Unknown
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method Literal[C].f() -> Unknown
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: bound method type[C].f() -> Unknown
```
The `owner` argument takes precedence over the `instance` argument:
```py
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: <bound method `f` of `Literal[C]`>
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound method Literal[C].f() -> Unknown
```
### Classmethods mixed with other decorators
```toml
[environment]
python-version = "3.12"
```
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
class method:
@@ -410,29 +415,19 @@ def does_nothing[T](f: T) -> T:
class C:
@classmethod
# TODO: no error should be emitted here (needs support for generics)
# error: [invalid-argument-type]
@does_nothing
def f1(cls: type[C], x: int) -> str:
return "a"
# TODO: no error should be emitted here (needs support for generics)
# error: [invalid-argument-type]
@does_nothing
@classmethod
def f2(cls: type[C], x: int) -> str:
return "a"
# TODO: All of these should be `str` (and not emit an error), once we support generics
# error: [call-non-callable]
reveal_type(C.f1(1)) # revealed: Unknown
# error: [call-non-callable]
reveal_type(C().f1(1)) # revealed: Unknown
# error: [call-non-callable]
reveal_type(C.f2(1)) # revealed: Unknown
# error: [call-non-callable]
reveal_type(C().f2(1)) # revealed: Unknown
reveal_type(C.f1(1)) # revealed: str
reveal_type(C().f1(1)) # revealed: str
reveal_type(C.f2(1)) # revealed: str
reveal_type(C().f2(1)) # revealed: str
```
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods

View File

@@ -0,0 +1,50 @@
# `str.startswith`
We special-case `str.startswith` to allow inference of precise Boolean literal types, because those
are used in [`sys.platform` checks].
```py
reveal_type("abc".startswith("")) # revealed: Literal[True]
reveal_type("abc".startswith("a")) # revealed: Literal[True]
reveal_type("abc".startswith("ab")) # revealed: Literal[True]
reveal_type("abc".startswith("abc")) # revealed: Literal[True]
reveal_type("abc".startswith("abcd")) # revealed: Literal[False]
reveal_type("abc".startswith("bc")) # revealed: Literal[False]
reveal_type("AbC".startswith("")) # revealed: Literal[True]
reveal_type("AbC".startswith("A")) # revealed: Literal[True]
reveal_type("AbC".startswith("Ab")) # revealed: Literal[True]
reveal_type("AbC".startswith("AbC")) # revealed: Literal[True]
reveal_type("AbC".startswith("a")) # revealed: Literal[False]
reveal_type("AbC".startswith("aB")) # revealed: Literal[False]
reveal_type("".startswith("")) # revealed: Literal[True]
reveal_type("".startswith(" ")) # revealed: Literal[False]
```
Make sure that we fall back to `bool` for more complex cases:
```py
reveal_type("abc".startswith("b", 1)) # revealed: bool
reveal_type("abc".startswith("bc", 1, 3)) # revealed: bool
reveal_type("abc".startswith(("a", "x"))) # revealed: bool
```
And similiarly, we should still infer `bool` if the instance or the prefix are not string literals:
```py
from typing_extensions import LiteralString
def _(string_instance: str, literalstring: LiteralString):
reveal_type(string_instance.startswith("a")) # revealed: bool
reveal_type(literalstring.startswith("a")) # revealed: bool
reveal_type("a".startswith(string_instance)) # revealed: bool
reveal_type("a".startswith(literalstring)) # revealed: bool
```
[`sys.platform` checks]: https://docs.python.org/3/library/sys.html#sys.platform

View File

@@ -20,7 +20,7 @@ class C:
def _(subclass_of_c: type[C]):
reveal_type(subclass_of_c(1)) # revealed: C
# error: [invalid-argument-type] "Object of type `Literal["a"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `int`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["a"]`"
reveal_type(subclass_of_c("a")) # revealed: C
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(subclass_of_c()) # revealed: C

View File

@@ -94,7 +94,7 @@ def _(flag: bool):
else:
f = f2
# error: [invalid-argument-type] "Object of type `Literal[3]` cannot be assigned to parameter 1 (`a`) of function `f2`; expected type `str`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[3]`"
x = f(3)
reveal_type(x) # revealed: int | str
```
@@ -175,3 +175,41 @@ def _(flag: bool):
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
reveal_type(f(int)) # revealed: str | Literal[True]
```
## Size limit on unions of literals
Beyond a certain size, large unions of literal types collapse to their nearest super-type (`int`,
`bytes`, `str`).
```py
from typing import Literal
def _(literals_2: Literal[0, 1], b: bool, flag: bool):
literals_4 = 2 * literals_2 + literals_2 # Literal[0, 1, 2, 3]
literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15]
literals_64 = 4 * literals_16 + literals_4 # Literal[0, 1, .., 63]
literals_128 = 2 * literals_64 + literals_2 # Literal[0, 1, .., 127]
# Going beyond the MAX_UNION_LITERALS limit (currently 200):
literals_256 = 16 * literals_16 + literals_16
reveal_type(literals_256) # revealed: int
# Going beyond the limit when another type is already part of the union
bool_and_literals_128 = b if flag else literals_128 # bool | Literal[0, 1, ..., 127]
literals_128_shifted = literals_128 + 128 # Literal[128, 129, ..., 255]
# Now union the two:
reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int
```
## Simplifying gradually-equivalent types
If two types are gradually equivalent, we can keep just one of them in a union:
```py
from typing import Any, Union
from knot_extensions import Intersection, Not
def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]):
reveal_type(x) # revealed: Any & ~int
```

View File

@@ -0,0 +1,410 @@
# Super
Python defines the terms *bound super object* and *unbound super object*.
An **unbound super object** is created when `super` is called with only one argument. (e.g.
`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is
rarely used in practice.
A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the
implicit form `super()`, where both the pivot class and the owner are inferred. This is the most
common usage.
## Basic Usage
### Explicit Super Object
`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the
specified pivot class.
```py
class A:
def a(self): ...
aa: int = 1
class B(A):
def b(self): ...
bb: int = 2
class C(B):
def c(self): ...
cc: int = 3
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
super(C, C()).a
super(C, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[C], C>` has no attribute `c`"
super(C, C()).c
super(B, C()).a
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `b`"
super(B, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `c`"
super(B, C()).c
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `a`"
super(A, C()).a
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `b`"
super(A, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `c`"
super(A, C()).c
reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
reveal_type(super(C, C()).aa) # revealed: int
reveal_type(super(C, C()).bb) # revealed: int
```
### Implicit Super Object
The implicit form `super()` is same as `super(__class__, <first argument>)`. The `__class__` refers
to the class that contains the function where `super()` is used. The first argument refers to the
current methods first parameter (typically `self` or `cls`).
```py
from __future__ import annotations
class A:
def __init__(self, a: int): ...
@classmethod
def f(cls): ...
class B(A):
def __init__(self, a: int):
# TODO: Once `Self` is supported, this should be `<super: Literal[B], B>`
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
super().__init__(a)
@classmethod
def f(cls):
# TODO: Once `Self` is supported, this should be `<super: Literal[B], Literal[B]>`
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
super().f()
super(B, B(42)).__init__(42)
super(B, B).f()
```
### Unbound Super Object
Calling `super(cls)` without a second argument returns an *unbound super object*. This is treated as
a plain `super` instance and does not support name lookup via the MRO.
```py
class A:
a: int = 42
class B(A): ...
reveal_type(super(B)) # revealed: super
# error: [unresolved-attribute] "Type `super` has no attribute `a`"
super(B).a
```
## Attribute Assignment
`super()` objects do not allow attribute assignment — even if the attribute is resolved
successfully.
```py
class A:
a: int = 3
class B(A): ...
reveal_type(super(B, B()).a) # revealed: int
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: Literal[B], B>`"
super(B, B()).a = 3
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`"
super(B).a = 5
```
## Dynamic Types
If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a
member, it should effectively behave like a dynamic type.
```py
class A:
a: int = 1
def f(x):
reveal_type(x) # revealed: Unknown
reveal_type(super(x, x)) # revealed: <super: Unknown, Unknown>
reveal_type(super(A, x)) # revealed: <super: Literal[A], Unknown>
reveal_type(super(x, A())) # revealed: <super: Unknown, A>
reveal_type(super(x, x).a) # revealed: Unknown
reveal_type(super(A, x).a) # revealed: Unknown
reveal_type(super(x, A()).a) # revealed: Unknown
```
## Implicit `super()` in Complex Structure
```py
from __future__ import annotations
class A:
def test(self):
reveal_type(super()) # revealed: <super: Literal[A], Unknown>
class B:
def test(self):
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
class C(A.B):
def test(self):
reveal_type(super()) # revealed: <super: Literal[C], Unknown>
def inner(t: C):
reveal_type(super()) # revealed: <super: Literal[B], C>
lambda x: reveal_type(super()) # revealed: <super: Literal[B], Unknown>
```
## Built-ins and Literals
```py
reveal_type(super(bool, True)) # revealed: <super: Literal[bool], bool>
reveal_type(super(bool, bool())) # revealed: <super: Literal[bool], bool>
reveal_type(super(int, bool())) # revealed: <super: Literal[int], bool>
reveal_type(super(int, 3)) # revealed: <super: Literal[int], int>
reveal_type(super(str, "")) # revealed: <super: Literal[str], str>
```
## Descriptor Behavior with Super
Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can
differ depending on whether the second argument to `super` is a class or an instance.
```py
class A:
def a1(self): ...
@classmethod
def a2(cls): ...
class B(A): ...
# A.__dict__["a1"].__get__(B(), B)
reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown
# A.__dict__["a2"].__get__(B(), B)
reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown
# A.__dict__["a1"].__get__(None, B)
reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown
# A.__dict__["a2"].__get__(None, B)
reveal_type(super(B, B).a2) # revealed: bound method Literal[B].a2() -> Unknown
```
## Union of Supers
When the owner is a union type, `super()` is built separately for each branch, and the resulting
super objects are combined into a union.
```py
class A: ...
class B:
b: int = 42
class C(A, B): ...
class D(B, A): ...
def f(x: C | D):
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]]
s = super(A, x)
reveal_type(s) # revealed: <super: Literal[A], C> | <super: Literal[A], D>
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: Literal[A], C> | <super: Literal[A], D>` is possibly unbound"
s.b
def f(flag: bool):
x = str() if flag else str("hello")
reveal_type(x) # revealed: Literal["", "hello"]
reveal_type(super(str, x)) # revealed: <super: Literal[str], str>
def f(x: int | str):
# error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call"
super(int, x)
```
Even when `super()` is constructed separately for each branch of a union, it should behave correctly
in all cases.
```py
def f(flag: bool):
if flag:
class A:
x = 1
y: int = 1
a: str = "hello"
class B(A): ...
s = super(B, B())
else:
class C:
x = 2
y: int | str = "test"
class D(C): ...
s = super(D, D())
reveal_type(s) # revealed: <super: Literal[B], B> | <super: Literal[D], D>
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
reveal_type(s.y) # revealed: int | str
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: Literal[B], B> | <super: Literal[D], D>` is possibly unbound"
reveal_type(s.a) # revealed: str
```
## Supers with Generic Classes
```toml
[environment]
python-version = "3.12"
```
```py
from knot_extensions import TypeOf, static_assert, is_subtype_of
class A[T]:
def f(self, a: T) -> T:
return a
class B[T](A[T]):
def f(self, b: T) -> T:
return super().f(b)
```
## Invalid Usages
### Unresolvable `super()` Calls
If an appropriate class and argument cannot be found, a runtime error will occur.
```py
from __future__ import annotations
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
reveal_type(super()) # revealed: Unknown
def f():
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
super()
# No first argument in its scope
class A:
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
s = super()
def f(self):
def g():
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
super()
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
lambda: super()
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
(super() for _ in range(10))
@staticmethod
def h():
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
super()
```
### Failing Condition Checks
```toml
[environment]
python-version = "3.12"
```
`super()` requires its first argument to be a valid class, and its second argument to be either an
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
runtime.
```py
def f(x: int):
# error: [invalid-super-argument] "`int` is not a valid class"
super(x, x)
type IntAlias = int
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
super(IntAlias, 0)
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call"
# revealed: Unknown
reveal_type(super(int, str()))
# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call"
# revealed: Unknown
reveal_type(super(int, str))
class A: ...
class B(A): ...
# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call"
# revealed: Unknown
reveal_type(super(B, A()))
# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call"
# revealed: Unknown
reveal_type(super(B, object()))
# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call"
# revealed: Unknown
reveal_type(super(B, A))
# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call"
# revealed: Unknown
reveal_type(super(B, object))
super(object, object()).__class__
```
### Instance Member Access via `super`
Accessing instance members through `super()` is not allowed.
```py
from __future__ import annotations
class A:
def __init__(self, a: int):
self.a = a
class B(A):
def __init__(self, a: int):
super().__init__(a)
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
super().a
# error: [unresolved-attribute] "Type `<super: Literal[B], B>` has no attribute `a`"
super(B, B(42)).a
```
### Dunder Method Resolution
Dunder methods defined in the `owner` (from `super(pivot_class, owner)`) should not affect the super
object itself. In other words, `super` should not be treated as if it inherits attributes of the
`owner`.
```py
class A:
def __getitem__(self, key: int) -> int:
return 42
class B(A): ...
reveal_type(A()[0]) # revealed: int
reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int
# error: [non-subscriptable] "Cannot subscript object of type `<super: Literal[B], B>` with no `__getitem__` method"
super(B, B())[0]
```

View File

@@ -50,13 +50,17 @@ reveal_type(x) # revealed: LiteralString
if x != "abc":
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
reveal_type(x == "abc") # revealed: Literal[False]
reveal_type("abc" == x) # revealed: Literal[False]
# TODO: This should be `Literal[False]`
reveal_type(x == "abc") # revealed: bool
# TODO: This should be `Literal[False]`
reveal_type("abc" == x) # revealed: bool
reveal_type(x == "something else") # revealed: bool
reveal_type("something else" == x) # revealed: bool
reveal_type(x != "abc") # revealed: Literal[True]
reveal_type("abc" != x) # revealed: Literal[True]
# TODO: This should be `Literal[True]`
reveal_type(x != "abc") # revealed: bool
# TODO: This should be `Literal[True]`
reveal_type("abc" != x) # revealed: bool
reveal_type(x != "something else") # revealed: bool
reveal_type("something else" != x) # revealed: bool
@@ -79,10 +83,10 @@ def _(x: int):
if x != 1:
reveal_type(x) # revealed: int & ~Literal[1]
reveal_type(x != 1) # revealed: Literal[True]
reveal_type(x != 1) # revealed: bool
reveal_type(x != 2) # revealed: bool
reveal_type(x == 1) # revealed: Literal[False]
reveal_type(x == 1) # revealed: bool
reveal_type(x == 2) # revealed: bool
```

View File

@@ -1,5 +1,10 @@
# Pattern matching
```toml
[environment]
python-version = "3.10"
```
## With wildcard
```py

View File

@@ -0,0 +1,731 @@
# Dataclasses
## Basic
Decorating a class with `@dataclass` is a convenient way to add special methods such as `__init__`,
`__repr__`, and `__eq__` to a class. The following example shows the basic usage of the `@dataclass`
decorator. By default, only the three mentioned methods are generated.
```py
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int | None = None
alice1 = Person("Alice", 30)
alice2 = Person(name="Alice", age=30)
alice3 = Person(age=30, name="Alice")
alice4 = Person("Alice", age=30)
reveal_type(alice1) # revealed: Person
reveal_type(type(alice1)) # revealed: type[Person]
reveal_type(alice1.name) # revealed: str
reveal_type(alice1.age) # revealed: int | None
reveal_type(repr(alice1)) # revealed: str
reveal_type(alice1 == alice2) # revealed: bool
reveal_type(alice1 == "Alice") # revealed: bool
bob = Person("Bob")
bob2 = Person("Bob", None)
bob3 = Person(name="Bob")
bob4 = Person(name="Bob", age=None)
```
The signature of the `__init__` method is generated based on the classes attributes. The following
calls are not valid:
```py
# error: [missing-argument]
Person()
# error: [too-many-positional-arguments]
Person("Eve", 20, "too many arguments")
# error: [invalid-argument-type]
Person("Eve", "string instead of int")
# error: [invalid-argument-type]
# error: [invalid-argument-type]
Person(20, "Eve")
```
## Signature of `__init__`
TODO: All of the following tests are missing the `self` argument in the `__init__` signature.
Declarations in the class body are used to generate the signature of the `__init__` method. If the
attributes are not just declarations, but also bindings, the type inferred from bindings is used as
the default value.
```py
from dataclasses import dataclass
@dataclass
class D:
x: int
y: str = "default"
z: int | None = 1 + 2
reveal_type(D.__init__) # revealed: (x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
```
This also works if the declaration and binding are split:
```py
@dataclass
class D:
x: int | None
x = None
reveal_type(D.__init__) # revealed: (x: int | None = None) -> None
```
Non-fully static types are handled correctly:
```py
from typing import Any
@dataclass
class C:
x: Any
y: int | Any
z: tuple[int, Any]
reveal_type(C.__init__) # revealed: (x: Any, y: int | Any, z: tuple[int, Any]) -> None
```
Variables without annotations are ignored:
```py
@dataclass
class D:
x: int
y = 1
reveal_type(D.__init__) # revealed: (x: int) -> None
```
If attributes without default values are declared after attributes with default values, a
`TypeError` will be raised at runtime. Ideally, we would emit a diagnostic in that case:
```py
@dataclass
class D:
x: int = 1
# TODO: this should be an error: field without default defined after field with default
y: str
```
Pure class attributes (`ClassVar`) are not included in the signature of `__init__`:
```py
from typing import ClassVar
@dataclass
class D:
x: int
y: ClassVar[str] = "default"
z: bool
reveal_type(D.__init__) # revealed: (x: int, z: bool) -> None
d = D(1, True)
reveal_type(d.x) # revealed: int
reveal_type(d.y) # revealed: str
reveal_type(d.z) # revealed: bool
```
Function declarations do not affect the signature of `__init__`:
```py
@dataclass
class D:
x: int
def y(self) -> str:
return ""
reveal_type(D.__init__) # revealed: (x: int) -> None
```
And neither do nested class declarations:
```py
@dataclass
class D:
x: int
class Nested:
y: str
reveal_type(D.__init__) # revealed: (x: int) -> None
```
But if there is a variable annotation with a function or class literal type, the signature of
`__init__` will include this field:
```py
from knot_extensions import TypeOf
class SomeClass: ...
def some_function() -> None: ...
@dataclass
class D:
function_literal: TypeOf[some_function]
class_literal: TypeOf[SomeClass]
class_subtype_of: type[SomeClass]
# revealed: (function_literal: def some_function() -> None, class_literal: Literal[SomeClass], class_subtype_of: type[SomeClass]) -> None
reveal_type(D.__init__)
```
More realistically, dataclasses can have `Callable` attributes:
```py
from typing import Callable
@dataclass
class D:
c: Callable[[int], str]
reveal_type(D.__init__) # revealed: (c: (int, /) -> str) -> None
```
Implicit instance attributes do not affect the signature of `__init__`:
```py
@dataclass
class D:
x: int
def f(self, y: str) -> None:
self.y: str = y
reveal_type(D(1).y) # revealed: str
reveal_type(D.__init__) # revealed: (x: int) -> None
```
Annotating expressions does not lead to an entry in `__annotations__` at runtime, and so it wouldn't
be included in the signature of `__init__`. This is a case that we currently don't detect:
```py
@dataclass
class D:
# (x) is an expression, not a "simple name"
(x): int = 1
# TODO: should ideally not include a `x` parameter
reveal_type(D.__init__) # revealed: (x: int = Literal[1]) -> None
```
## `@dataclass` calls with arguments
The `@dataclass` decorator can take several arguments to customize the existence of the generated
methods. The following test makes sure that we still treat the class as a dataclass if (the default)
arguments are passed in:
```py
from dataclasses import dataclass
@dataclass(init=True, repr=True, eq=True)
class Person:
name: str
age: int | None = None
alice = Person("Alice", 30)
reveal_type(repr(alice)) # revealed: str
reveal_type(alice == alice) # revealed: bool
```
If `init` is set to `False`, no `__init__` method is generated:
```py
from dataclasses import dataclass
@dataclass(init=False)
class C:
x: int
C() # Okay
# error: [too-many-positional-arguments]
C(1)
repr(C())
C() == C()
```
## Other dataclass parameters
### `repr`
A custom `__repr__` method is generated by default. It can be disabled by passing `repr=False`, but
in that case `__repr__` is still available via `object.__repr__`:
```py
from dataclasses import dataclass
@dataclass(repr=False)
class WithoutRepr:
x: int
reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str
```
### `eq`
The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__` method, but
`__eq__` is still available via `object.__eq__`:
```py
from dataclasses import dataclass
@dataclass(eq=False)
class WithoutEq:
x: int
reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
```
### `order`
```toml
[environment]
python-version = "3.12"
```
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
methods will be generated:
```py
from dataclasses import dataclass
@dataclass
class WithoutOrder:
x: int
WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator]
WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator]
@dataclass(order=True)
class WithOrder:
x: int
WithOrder(1) < WithOrder(2)
WithOrder(1) <= WithOrder(2)
WithOrder(1) > WithOrder(2)
WithOrder(1) >= WithOrder(2)
```
Comparisons are only allowed for `WithOrder` instances:
```py
WithOrder(1) < 2 # error: [unsupported-operator]
WithOrder(1) <= 2 # error: [unsupported-operator]
WithOrder(1) > 2 # error: [unsupported-operator]
WithOrder(1) >= 2 # error: [unsupported-operator]
```
This also works for generic dataclasses:
```py
from dataclasses import dataclass
@dataclass(order=True)
class GenericWithOrder[T]:
x: T
GenericWithOrder[int](1) < GenericWithOrder[int](1)
GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator]
```
If a class already defines one of the comparison methods, a `TypeError` is raised at runtime.
Ideally, we would emit a diagnostic in that case:
```py
@dataclass(order=True)
class AlreadyHasCustomDunderLt:
x: int
# TODO: Ideally, we would emit a diagnostic here
def __lt__(self, other: object) -> bool:
return False
```
### `unsafe_hash`
To do
### `frozen`
To do
### `match_args`
To do
### `kw_only`
To do
### `slots`
To do
### `weakref_slot`
To do
## Inheritance
### Normal class inheriting from a dataclass
```py
from dataclasses import dataclass
@dataclass
class Base:
x: int
class Derived(Base): ...
d = Derived(1) # OK
reveal_type(d.x) # revealed: int
```
### Dataclass inheriting from normal class
```py
from dataclasses import dataclass
class Base:
x: int = 1
@dataclass
class Derived(Base):
y: str
d = Derived("a")
# error: [too-many-positional-arguments]
# error: [invalid-argument-type]
Derived(1, "a")
```
### Dataclass inheriting from another dataclass
```py
from dataclasses import dataclass
@dataclass
class Base:
x: int
y: str
@dataclass
class Derived(Base):
z: bool
d = Derived(1, "a", True) # OK
reveal_type(d.x) # revealed: int
reveal_type(d.y) # revealed: str
reveal_type(d.z) # revealed: bool
# error: [missing-argument]
Derived(1, "a")
# error: [missing-argument]
Derived(True)
```
### Overwriting attributes from base class
The following example comes from the
[Python documentation](https://docs.python.org/3/library/dataclasses.html#inheritance). The `x`
attribute appears just once in the `__init__` signature, and the default value is taken from the
derived class
```py
from dataclasses import dataclass
from typing import Any
@dataclass
class Base:
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 15
reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
```
## Generic dataclasses
```toml
[environment]
python-version = "3.12"
```
```py
from dataclasses import dataclass
@dataclass
class DataWithDescription[T]:
data: T
description: str
reveal_type(DataWithDescription[int]) # revealed: Literal[DataWithDescription[int]]
d_int = DataWithDescription[int](1, "description") # OK
reveal_type(d_int.data) # revealed: int
reveal_type(d_int.description) # revealed: str
# error: [invalid-argument-type]
DataWithDescription[int](None, "description")
```
## Descriptor-typed fields
### Same type in `__get__` and `__set__`
For the following descriptor, the return type of `__get__` and the type of the `value` parameter in
`__set__` are the same. The generated `__init__` method takes an argument of this type (instead of
the type of the descriptor), and the default value is also of this type:
```py
from typing import overload
from dataclasses import dataclass
class UppercaseString:
_value: str = ""
def __get__(self, instance: object, owner: None | type) -> str:
return self._value
def __set__(self, instance: object, value: str) -> None:
self._value = value.upper()
@dataclass
class C:
upper: UppercaseString = UppercaseString()
reveal_type(C.__init__) # revealed: (upper: str = str) -> None
c = C("abc")
reveal_type(c.upper) # revealed: str
# This is also okay:
C()
# error: [invalid-argument-type]
C(1)
# error: [too-many-positional-arguments]
C("a", "b")
```
### Different types in `__get__` and `__set__`
In general, the type of the `__init__` parameter is determined by the `value` parameter type of the
`__set__` method (`str` in the example below). However, the default value is generated by calling
the descriptor's `__get__` method as if it had been called on the class itself, i.e. passing `None`
for the `instance` argument.
```py
from typing import Literal, overload
from dataclasses import dataclass
class ConvertToLength:
_len: int = 0
@overload
def __get__(self, instance: None, owner: type) -> Literal[""]: ...
@overload
def __get__(self, instance: object, owner: type | None) -> int: ...
def __get__(self, instance: object | None, owner: type | None) -> str | int:
if instance is None:
return ""
return self._len
def __set__(self, instance, value: str) -> None:
self._len = len(value)
@dataclass
class C:
converter: ConvertToLength = ConvertToLength()
reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None
c = C("abc")
reveal_type(c.converter) # revealed: int
# This is also okay:
C()
# error: [invalid-argument-type]
C(1)
# error: [too-many-positional-arguments]
C("a", "b")
```
### With overloaded `__set__` method
If the `__set__` method is overloaded, we determine the type for the `__init__` parameter as the
union of all possible `value` parameter types:
```py
from typing import overload
from dataclasses import dataclass
class AcceptsStrAndInt:
def __get__(self, instance, owner) -> int:
return 0
@overload
def __set__(self, instance: object, value: str) -> None: ...
@overload
def __set__(self, instance: object, value: int) -> None: ...
def __set__(self, instance: object, value) -> None:
pass
@dataclass
class C:
field: AcceptsStrAndInt = AcceptsStrAndInt()
reveal_type(C.__init__) # revealed: (field: str | int = int) -> None
```
## `dataclasses.field`
To do
## Other special cases
### `dataclasses.dataclass`
We also understand dataclasses if they are decorated with the fully qualified name:
```py
import dataclasses
@dataclasses.dataclass
class C:
x: str
reveal_type(C.__init__) # revealed: (x: str) -> None
```
### Dataclass with custom `__init__` method
If a class already defines `__init__`, it is not replaced by the `dataclass` decorator.
```py
from dataclasses import dataclass
@dataclass(init=True)
class C:
x: str
def __init__(self, x: int) -> None:
self.x = str(x)
C(1) # OK
# error: [invalid-argument-type]
C("a")
```
Similarly, if we set `init=False`, we still recognize the custom `__init__` method:
```py
@dataclass(init=False)
class D:
def __init__(self, x: int) -> None:
self.x = str(x)
D(1) # OK
D() # error: [missing-argument]
```
### Accessing instance attributes on the class itself
Just like for normal classes, accessing instance attributes on the class itself is not allowed:
```py
from dataclasses import dataclass
@dataclass
class C:
x: int
# error: [unresolved-attribute] "Attribute `x` can only be accessed on instances, not on the class object `Literal[C]` itself."
C.x
```
### Return type of `dataclass(...)`
A call like `dataclass(order=True)` returns a callable itself, which is then used as the decorator.
We can store the callable in a variable and later use it as a decorator:
```py
from dataclasses import dataclass
dataclass_with_order = dataclass(order=True)
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclasses.dataclass>
@dataclass_with_order
class C:
x: int
C(1) < C(2) # ok
```
### Using `dataclass` as a function
To do
## Internals
The `dataclass` decorator returns the class itself. This means that the type of `Person` is `type`,
and attributes like the MRO are unchanged:
```py
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int | None = None
reveal_type(type(Person)) # revealed: Literal[type]
reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]]
```
The generated methods have the following signatures:
```py
# TODO: `self` is missing here
reveal_type(Person.__init__) # revealed: (name: str, age: int | None = None) -> None
reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str
reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool
```

View File

@@ -145,10 +145,10 @@ def f(x: int) -> int:
return x**2
# TODO: Should be `_lru_cache_wrapper[int]`
reveal_type(f) # revealed: @Todo(generics)
reveal_type(f) # revealed: @Todo(specialized non-generic class)
# TODO: Should be `int`
reveal_type(f(1)) # revealed: @Todo(generics)
reveal_type(f(1)) # revealed: @Todo(specialized non-generic class)
```
## Lambdas as decorators
@@ -207,7 +207,7 @@ first argument:
def wrong_signature(f: int) -> str:
return "a"
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 1 (`f`) of function `wrong_signature`; expected type `int`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `def f(x) -> Unknown`"
@wrong_signature
def f(x): ...

View File

@@ -459,11 +459,9 @@ class Descriptor:
class C:
d: Descriptor = Descriptor()
# TODO: should be `Literal["called on class object"]
reveal_type(C.d) # revealed: LiteralString
reveal_type(C.d) # revealed: Literal["called on class object"]
# TODO: should be `Literal["called on instance"]
reveal_type(C().d) # revealed: LiteralString
reveal_type(C().d) # revealed: Literal["called on instance"]
```
## Descriptor protocol for dunder methods
@@ -563,18 +561,18 @@ from inspect import getattr_static
def f(x: object) -> str:
return "a"
reveal_type(f) # revealed: Literal[f]
reveal_type(f) # revealed: def f(x: object) -> str
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(f.__get__(None, type(f))) # revealed: Literal[f]
reveal_type(f.__get__(None, type(f))) # revealed: def f(x: object) -> str
reveal_type(f.__get__(None, type(f))(1)) # revealed: str
wrapper_descriptor = getattr_static(f, "__get__")
reveal_type(wrapper_descriptor) # revealed: <wrapper-descriptor `__get__` of `function` objects>
reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: Literal[f]
reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: def f(x: object) -> str
# Attribute access on the method-wrapper `f.__get__` falls back to `MethodWrapperType`:
reveal_type(f.__get__.__hash__) # revealed: <bound method `__hash__` of `MethodWrapperType`>
reveal_type(f.__get__.__hash__) # revealed: bound method MethodWrapperType.__hash__() -> int
# Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`:
reveal_type(wrapper_descriptor.__qualname__) # revealed: str
@@ -587,7 +585,7 @@ class C: ...
bound_method = wrapper_descriptor(f, C(), C)
reveal_type(bound_method) # revealed: <bound method `f` of `C`>
reveal_type(bound_method) # revealed: bound method C.f() -> str
```
We can then call it, and the instance of `C` is implicitly passed to the first parameter of `f`

View File

@@ -0,0 +1,37 @@
# Version-related syntax error diagnostics
## `match` statement
The `match` statement was introduced in Python 3.10.
### Before 3.10
<!-- snapshot-diagnostics -->
We should emit a syntax error before 3.10.
```toml
[environment]
python-version = "3.9"
```
```py
match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
case 1:
print("it's one")
```
### After 3.10
On or after 3.10, no error should be reported.
```toml
[environment]
python-version = "3.10"
```
```py
match 2:
case 1:
print("it's one")
```

View File

@@ -26,6 +26,11 @@ def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
## Use case: Type narrowing and exhaustiveness checking
```toml
[environment]
python-version = "3.10"
```
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.

View File

@@ -61,7 +61,7 @@ from knot_extensions import Unknown
def f(x: Any, y: Unknown, z: Any | str | int):
a = cast(dict[str, Any], x)
reveal_type(a) # revealed: @Todo(generics)
reveal_type(a) # revealed: @Todo(specialized non-generic class)
b = cast(Any, y)
reveal_type(b) # revealed: Any

View File

@@ -42,7 +42,7 @@ def f(w: Wrapper) -> None:
v: int | None = w.value
# This function call is incorrect, because `w.value` could be `None`. We therefore emit the following
# error: "`Unknown | None` cannot be assigned to parameter 1 (`i`) of function `accepts_int`; expected type `int`"
# error: "Argument to this function is incorrect: Expected `int`, found `Unknown | None`"
c = accepts_int(w.value)
```

View File

@@ -591,9 +591,9 @@ try:
reveal_type(x) # revealed: B | D
reveal_type(x) # revealed: B | D
x = foo
reveal_type(x) # revealed: Literal[foo]
reveal_type(x) # revealed: def foo(param=A) -> Unknown
except:
reveal_type(x) # revealed: Literal[1] | Literal[foo]
reveal_type(x) # revealed: Literal[1] | (def foo(param=A) -> Unknown)
class Bar:
x = could_raise_returns_E()
@@ -603,9 +603,9 @@ except:
reveal_type(x) # revealed: Literal[Bar]
finally:
# TODO: should be `Literal[1] | Literal[foo] | Literal[Bar]`
reveal_type(x) # revealed: Literal[foo] | Literal[Bar]
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | Literal[Bar]
reveal_type(x) # revealed: Literal[foo] | Literal[Bar]
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | Literal[Bar]
```
[1]: https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d

View File

@@ -76,6 +76,11 @@ def g(x: Any = "foo"):
## Stub functions
```toml
[environment]
python-version = "3.12"
```
### In Protocol
```py

View File

@@ -56,6 +56,11 @@ def f() -> int:
### In Protocol
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Protocol, TypeVar
@@ -69,8 +74,6 @@ class Baz(Bar):
T = TypeVar("T")
class Qux(Protocol[T]):
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
class Foo(Protocol):
@@ -85,6 +88,11 @@ class Lorem(t[0]):
### In abstract method
```toml
[environment]
python-version = "3.12"
```
```py
from abc import ABC, abstractmethod
@@ -310,7 +318,7 @@ def f(cond: bool) -> str:
return "hello" if cond else NotImplemented
def f(cond: bool) -> int:
# error: [invalid-return-type] "Object of type `Literal["hello"]` is not assignable to return type `int`"
# error: [invalid-return-type] "Return type does not match returned value: Expected `int`, found `Literal["hello"]`"
return "hello" if cond else NotImplemented
```

View File

@@ -1,5 +1,10 @@
# Generic classes
```toml
[environment]
python-version = "3.13"
```
## PEP 695 syntax
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.
@@ -40,8 +45,6 @@ from typing import Generic, TypeVar
T = TypeVar("T")
# TODO: no error
# error: [invalid-base]
class C(Generic[T]): ...
```
@@ -81,10 +84,10 @@ class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
# error: [invalid-argument-type] "Object of type `str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]()) # revealed: Unknown
# error: [invalid-argument-type] "Object of type `int | str` cannot be assigned to parameter 1 (`T`) of class `Bounded`; expected type `int`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
@@ -110,7 +113,7 @@ reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
# error: [invalid-argument-type] "Object of type `object` cannot be assigned to parameter 1 (`T`) of class `Constrained`; expected type `int | str`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]()) # revealed: Unknown
```
@@ -149,23 +152,102 @@ If a typevar does not provide a default, we use `Unknown`:
reveal_type(C()) # revealed: C[Unknown]
```
## Inferring generic class parameters from constructors
If the type of a constructor parameter is a class typevar, we can use that to infer the type
parameter:
parameter. The types inferred from a type context and from a constructor parameter must be
consistent with each other.
## `__new__` only
```py
class E[T]:
def __init__(self, x: T) -> None: ...
class C[T]:
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
# TODO: revealed: E[int] or E[Literal[1]]
reveal_type(E(1)) # revealed: E[Unknown]
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
The types inferred from a type context and from a constructor parameter must be consistent with each
other:
## `__init__` only
```py
# TODO: error: [invalid-argument-type]
wrong_innards: E[int] = E("five")
class C[T]:
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
## Identical `__new__` and `__init__` signatures
```py
class C[T]:
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
## Compatible `__new__` and `__init__` signatures
```py
class C[T]:
def __new__(cls, *args, **kwargs) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D[T]:
def __new__(cls, x: T) -> "D[T]":
return object.__new__(cls)
def __init__(self, *args, **kwargs) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
## `__init__` is itself generic
TODO: These do not currently work yet, because we don't correctly model the nested generic contexts.
```py
class C[T]:
def __init__[S](self, x: T, y: S) -> None: ...
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, 1)) # revealed: C[Unknown]
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, "string")) # revealed: C[Unknown]
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, True)) # revealed: C[Unknown]
# TODO: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`"
wrong_innards: C[int] = C("five", 1)
```
## Generic subclass
@@ -200,10 +282,7 @@ class C[T]:
def cannot_shadow_class_typevar[T](self, t: T): ...
c: C[int] = C[int]()
# TODO: no error
# TODO: revealed: str or Literal["string"]
# error: [invalid-argument-type]
reveal_type(c.method("string")) # revealed: U
reveal_type(c.method("string")) # revealed: Literal["string"]
```
## Cyclic class definition

View File

@@ -1,5 +1,10 @@
# Generic functions
```toml
[environment]
python-version = "3.12"
```
## Typevar must be used at least twice
If you're only using a typevar for a single parameter, you don't need the typevar — just use
@@ -43,33 +48,14 @@ def absurd[T]() -> T:
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
is bound to at each call site.
TODO: Note that some of the TODO revealed types have two options, since we haven't decided yet
whether we want to infer a more specific `Literal` type where possible, or use heuristics to weaken
the inferred type to e.g. `int`.
```py
def f[T](x: T) -> T:
return x
# TODO: no error
# TODO: revealed: int or Literal[1]
# error: [invalid-argument-type]
reveal_type(f(1)) # revealed: T
# TODO: no error
# TODO: revealed: float
# error: [invalid-argument-type]
reveal_type(f(1.0)) # revealed: T
# TODO: no error
# TODO: revealed: bool or Literal[true]
# error: [invalid-argument-type]
reveal_type(f(True)) # revealed: T
# TODO: no error
# TODO: revealed: str or Literal["string"]
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: T
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(1.0)) # revealed: float
reveal_type(f(True)) # revealed: Literal[True]
reveal_type(f("string")) # revealed: Literal["string"]
```
## Inferring “deep” generic parameter types
@@ -82,7 +68,7 @@ def f[T](x: list[T]) -> T:
return x[0]
# TODO: revealed: float
reveal_type(f([1.0, 2.0])) # revealed: T
reveal_type(f([1.0, 2.0])) # revealed: Unknown
```
## Typevar constraints
@@ -93,7 +79,6 @@ in the function.
```py
def good_param[T: int](x: T) -> None:
# TODO: revealed: T & int
reveal_type(x) # revealed: T
```
@@ -107,7 +92,7 @@ def good_return[T: int](x: T) -> T:
return x
def bad_return[T: int](x: T) -> T:
# error: [invalid-return-type] "Object of type `int` is not assignable to return type `T`"
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`"
return x + 1
```
@@ -120,7 +105,7 @@ def different_types[T, S](cond: bool, t: T, s: S) -> T:
if cond:
return t
else:
# error: [invalid-return-type] "Object of type `S` is not assignable to return type `T`"
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`"
return s
def same_types[T](cond: bool, t1: T, t2: T) -> T:
@@ -162,61 +147,41 @@ parameters simultaneously.
def two_params[T](x: T, y: T) -> T:
return x
# TODO: no error
# TODO: revealed: str
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(two_params("a", "b")) # revealed: T
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
```
# TODO: no error
# TODO: revealed: str | int
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(two_params("a", 1)) # revealed: T
When one of the parameters is a union, we attempt to find the smallest specialization that satisfies
all of the constraints.
```py
def union_param[T](x: T | None) -> T:
if x is None:
raise ValueError
return x
reveal_type(union_param("a")) # revealed: Literal["a"]
reveal_type(union_param(1)) # revealed: Literal[1]
reveal_type(union_param(None)) # revealed: Unknown
```
```py
def param_with_union[T](x: T | int, y: T) -> T:
def union_and_nonunion_params[T](x: T | int, y: T) -> T:
return y
# TODO: no error
# TODO: revealed: str
# error: [invalid-argument-type]
reveal_type(param_with_union(1, "a")) # revealed: T
# TODO: no error
# TODO: revealed: str
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(param_with_union("a", "a")) # revealed: T
# TODO: no error
# TODO: revealed: int
# error: [invalid-argument-type]
reveal_type(param_with_union(1, 1)) # revealed: T
# TODO: no error
# TODO: revealed: str | int
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(param_with_union("a", 1)) # revealed: T
reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"]
reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"]
reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1]
reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1]
reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1]
```
```py
def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]:
return y
# TODO: no error
# TODO: revealed: tuple[str, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[T, S]
# TODO: no error
# TODO: revealed: tuple[str, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[T, S]
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
```
## Inferring nested generic function calls
@@ -231,15 +196,6 @@ def f[T](x: T) -> tuple[T, int]:
def g[T](x: T) -> T | None:
return x
# TODO: no error
# TODO: revealed: tuple[str | None, int]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(f(g("a"))) # revealed: tuple[T, int]
# TODO: no error
# TODO: revealed: tuple[str, int] | None
# error: [invalid-argument-type]
# error: [invalid-argument-type]
reveal_type(g(f("a"))) # revealed: T | None
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
```

View File

@@ -1,5 +1,10 @@
# PEP 695 Generics
```toml
[environment]
python-version = "3.12"
```
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
## Type variables
@@ -59,19 +64,19 @@ is.)
from knot_extensions import is_fully_static, static_assert
from typing import Any
def unbounded_unconstrained[T](t: list[T]) -> None:
def unbounded_unconstrained[T](t: T) -> None:
static_assert(is_fully_static(T))
def bounded[T: int](t: list[T]) -> None:
def bounded[T: int](t: T) -> None:
static_assert(is_fully_static(T))
def bounded_by_gradual[T: Any](t: list[T]) -> None:
def bounded_by_gradual[T: Any](t: T) -> None:
static_assert(not is_fully_static(T))
def constrained[T: (int, str)](t: list[T]) -> None:
def constrained[T: (int, str)](t: T) -> None:
static_assert(is_fully_static(T))
def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None:
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
static_assert(not is_fully_static(T))
```
@@ -94,7 +99,7 @@ class Base(Super): ...
class Sub(Base): ...
class Unrelated: ...
def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None:
def unbounded_unconstrained[T, U](t: T, u: U) -> None:
static_assert(is_assignable_to(T, T))
static_assert(is_assignable_to(T, object))
static_assert(not is_assignable_to(T, Super))
@@ -124,7 +129,7 @@ is a final class, since the typevar can still be specialized to `Never`.)
from typing import Any
from typing_extensions import final
def bounded[T: Super](t: list[T]) -> None:
def bounded[T: Super](t: T) -> None:
static_assert(is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Sub))
static_assert(not is_assignable_to(Super, T))
@@ -135,7 +140,7 @@ def bounded[T: Super](t: list[T]) -> None:
static_assert(not is_subtype_of(Super, T))
static_assert(not is_subtype_of(Sub, T))
def bounded_by_gradual[T: Any](t: list[T]) -> None:
def bounded_by_gradual[T: Any](t: T) -> None:
static_assert(is_assignable_to(T, Any))
static_assert(is_assignable_to(Any, T))
static_assert(is_assignable_to(T, Super))
@@ -153,7 +158,7 @@ def bounded_by_gradual[T: Any](t: list[T]) -> None:
@final
class FinalClass: ...
def bounded_final[T: FinalClass](t: list[T]) -> None:
def bounded_final[T: FinalClass](t: T) -> None:
static_assert(is_assignable_to(T, FinalClass))
static_assert(not is_assignable_to(FinalClass, T))
@@ -167,14 +172,14 @@ true even if both typevars are bounded by the same final class, since you can sp
typevars to `Never` in addition to that final class.
```py
def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None:
def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
static_assert(not is_subtype_of(T, U))
static_assert(not is_subtype_of(U, T))
def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None:
def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
@@ -189,7 +194,7 @@ intersection of all of its constraints is a subtype of the typevar.
```py
from knot_extensions import Intersection
def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
def constrained[T: (Base, Unrelated)](t: T) -> None:
static_assert(not is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Sub))
@@ -214,7 +219,7 @@ def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
static_assert(not is_subtype_of(Super | Unrelated, T))
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None:
def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
static_assert(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Sub))
@@ -256,7 +261,7 @@ distinct constraints, meaning that there is (still) no guarantee that they will
the same type.
```py
def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None:
def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
@@ -266,7 +271,7 @@ def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> Non
@final
class AnotherFinalClass: ...
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None:
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None:
static_assert(not is_assignable_to(T, U))
static_assert(not is_assignable_to(U, T))
@@ -285,7 +290,7 @@ non-singleton type.
```py
from knot_extensions import is_singleton, is_single_valued, static_assert
def unbounded_unconstrained[T](t: list[T]) -> None:
def unbounded_unconstrained[T](t: T) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
```
@@ -294,7 +299,7 @@ A bounded typevar is not a singleton, even if its bound is a singleton, since it
specialized to `Never`.
```py
def bounded[T: None](t: list[T]) -> None:
def bounded[T: None](t: T) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
```
@@ -305,14 +310,14 @@ specialize a constrained typevar to a subtype of a constraint.)
```py
from typing_extensions import Literal
def constrained_non_singletons[T: (int, str)](t: list[T]) -> None:
def constrained_non_singletons[T: (int, str)](t: T) -> None:
static_assert(not is_singleton(T))
static_assert(not is_single_valued(T))
def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None:
def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None:
static_assert(is_singleton(T))
def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None:
def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None:
static_assert(is_single_valued(T))
```
@@ -507,6 +512,20 @@ def remove_constraint[T: (int, str, bool)](t: T) -> None:
reveal_type(x) # revealed: T & Any
```
The intersection of a typevar with any other type is assignable to (and if fully static, a subtype
of) itself.
```py
from knot_extensions import is_assignable_to, is_subtype_of, static_assert, Not
def intersection_is_assignable[T](t: T) -> None:
static_assert(is_assignable_to(Intersection[T, None], T))
static_assert(is_assignable_to(Intersection[T, Not[None]], T))
static_assert(is_subtype_of(Intersection[T, None], T))
static_assert(is_subtype_of(Intersection[T, Not[None]], T))
```
## Narrowing
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:

View File

@@ -1,5 +1,10 @@
# Scoping rules for type variables
```toml
[environment]
python-version = "3.12"
```
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
spec.
@@ -59,14 +64,8 @@ to a different type each time.
def f[T](x: T) -> T:
return x
# TODO: no error
# TODO: revealed: int or Literal[1]
# error: [invalid-argument-type]
reveal_type(f(1)) # revealed: T
# TODO: no error
# TODO: revealed: str or Literal["a"]
# error: [invalid-argument-type]
reveal_type(f("a")) # revealed: T
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f("a")) # revealed: Literal["a"]
```
## Methods can mention class typevars
@@ -85,7 +84,7 @@ class C[T]:
c: C[int] = C[int]()
c.m1(1)
c.m2(1)
# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `int`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["string"]`"
c.m2("string")
```
@@ -102,18 +101,18 @@ class C[T]:
def f(self, x: T) -> str:
return "a"
reveal_type(getattr_static(C[int], "f")) # revealed: Literal[f[int]]
reveal_type(getattr_static(C[int], "f")) # revealed: def f(self, x: int) -> str
reveal_type(getattr_static(C[int], "f").__get__) # revealed: <method-wrapper `__get__` of `f[int]`>
reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[f[int]]
# revealed: <bound method `f` of `C[int]`>
reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: def f(self, x: int) -> str
# revealed: bound method C[int].f(x: int) -> str
reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int]))
reveal_type(C[int].f) # revealed: Literal[f[int]]
reveal_type(C[int]().f) # revealed: <bound method `f` of `C[int]`>
reveal_type(C[int].f) # revealed: def f(self, x: int) -> str
reveal_type(C[int]().f) # revealed: bound method C[int].f(x: int) -> str
bound_method = C[int]().f
reveal_type(bound_method.__self__) # revealed: C[int]
reveal_type(bound_method.__func__) # revealed: Literal[f[int]]
reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str
reveal_type(C[int]().f(1)) # revealed: str
reveal_type(bound_method(1)) # revealed: str
@@ -124,7 +123,7 @@ reveal_type(C[int].f(C[int](), 1)) # revealed: str
class D[U](C[U]):
pass
reveal_type(D[int]().f) # revealed: <bound method `f` of `D[int]`>
reveal_type(D[int]().f) # revealed: bound method D[int].f(x: int) -> str
```
## Methods can mention other typevars
@@ -138,8 +137,6 @@ from typing import TypeVar, Generic
T = TypeVar("T")
S = TypeVar("S")
# TODO: no error
# error: [invalid-base]
class Legacy(Generic[T]):
def m(self, x: T, y: S) -> S:
return y
@@ -157,10 +154,7 @@ class C[T]:
return y
c: C[int] = C()
# TODO: no errors
# TODO: revealed: str
# error: [invalid-argument-type]
reveal_type(c.m(1, "string")) # revealed: S
reveal_type(c.m(1, "string")) # revealed: Literal["string"]
```
## Unbound typevars
@@ -178,13 +172,11 @@ S = TypeVar("S")
def f(x: T) -> None:
x: list[T] = []
# TODO: error
# TODO: invalid-assignment error
y: list[S] = []
# TODO: no error
# error: [invalid-base]
class C(Generic[T]):
# TODO: error
# TODO: error: cannot use S if it's not in the current generic context
x: list[S] = []
# This is not an error, as shown in the previous test
@@ -204,11 +196,11 @@ S = TypeVar("S")
def f[T](x: T) -> None:
x: list[T] = []
# TODO: error
# TODO: invalid assignment error
y: list[S] = []
class C[T]:
# TODO: error
# TODO: error: cannot use S if it's not in the current generic context
x: list[S] = []
def m1(self, x: S) -> S:
@@ -263,8 +255,7 @@ def f[T](x: T, y: T) -> None:
class Ok[S]: ...
# TODO: error for reuse of typevar
class Bad1[T]: ...
# TODO: no non-subscriptable error, error for reuse of typevar
# error: [non-subscriptable]
# TODO: error for reuse of typevar
class Bad2(Iterable[T]): ...
```
@@ -277,8 +268,7 @@ class C[T]:
class Ok1[S]: ...
# TODO: error for reuse of typevar
class Bad1[T]: ...
# TODO: no non-subscriptable error, error for reuse of typevar
# error: [non-subscriptable]
# TODO: error for reuse of typevar
class Bad2(Iterable[T]): ...
```
@@ -292,7 +282,7 @@ class C[T]:
ok1: list[T] = []
class Bad:
# TODO: error
# TODO: error: cannot refer to T in nested scope
bad: list[T] = []
class Inner[S]: ...

View File

@@ -0,0 +1,277 @@
# Variance
```toml
[environment]
python-version = "3.12"
```
Type variables have a property called _variance_ that affects the subtyping and assignability
relations. Much more detail can be found in the [spec]. To summarize, each typevar is either
**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not
currently mentioned in the typing spec, but is a fourth case that we must consider.)
For all of the examples below, we will consider a typevar `T`, a generic class using that typevar
`C[T]`, and two types `A` and `B`.
## Covariance
With a covariant typevar, subtyping is in "alignment": if `A <: B`, then `C[A] <: C[B]`.
Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of
`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would
get from the sequence is a valid `int`.
```py
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
class B(A): ...
class C[T]:
def receive(self) -> T:
raise ValueError
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A]))
static_assert(not is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
```
## Contravariance
With a contravariant typevar, subtyping is in "opposition": if `A <: B`, then `C[B] <: C[A]`.
Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives
`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool`
that you pass into the consumer is a valid `int`.
```py
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
class B(A): ...
class C[T]:
def send(self, value: T): ...
static_assert(not is_assignable_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
static_assert(not is_subtype_of(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A]))
static_assert(not is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
```
## Invariance
With an invariant typevar, _no_ specializations of the generic class are subtypes of each other.
This often occurs for types that are both producers _and_ consumers, like a mutable `list`.
Iterating over the elements in a list would work with a covariant typevar, just like with the
"producer" type above. Appending elements to a list would work with a contravariant typevar, just
like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant
at the same time!
If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list
of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list
would no longer only contain elements that are subtypes of `bool`.
Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a
mutable list of `int`s, since you might try to extract elements from the list: you expect every
element that you extract to be a subtype of `bool`, but the list can contain any `int`.
In the end, if you expect a mutable list, you must always be given a list of exactly that type,
since we can't know in advance which of the allowed methods you'll want to use.
```py
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
class B(A): ...
class C[T]:
def send(self, value: T): ...
def receive(self) -> T:
raise ValueError
static_assert(not is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
static_assert(not is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
static_assert(not is_equivalent_to(C[B], C[A]))
static_assert(not is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
```
## Bivariance
With a bivariant typevar, _all_ specializations of the generic class are subtypes of (and in fact,
equivalent to) each other.
This is a bit of pathological case, which really only happens when the class doesn't use the typevar
at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_
the typevar was used.)
```py
from knot_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
from typing import Any
class A: ...
class B(A): ...
class C[T]:
pass
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[A], C[B]))
static_assert(not is_equivalent_to(C[A], C[Any]))
static_assert(not is_equivalent_to(C[B], C[Any]))
static_assert(not is_equivalent_to(C[Any], C[A]))
static_assert(not is_equivalent_to(C[Any], C[B]))
static_assert(is_gradual_equivalent_to(C[A], C[A]))
static_assert(is_gradual_equivalent_to(C[B], C[B]))
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[A], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[A], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[B], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[Any], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_gradual_equivalent_to(C[Any], C[B]))
```
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance

View File

@@ -7,7 +7,7 @@ Builtin symbols can be explicitly imported:
```py
import builtins
reveal_type(builtins.chr) # revealed: Literal[chr]
reveal_type(builtins.chr) # revealed: def chr(i: int | SupportsIndex, /) -> str
```
## Implicit use of builtin
@@ -15,7 +15,7 @@ reveal_type(builtins.chr) # revealed: Literal[chr]
Or used implicitly:
```py
reveal_type(chr) # revealed: Literal[chr]
reveal_type(chr) # revealed: def chr(i: int | SupportsIndex, /) -> str
reveal_type(str) # revealed: Literal[str]
```

View File

@@ -103,8 +103,8 @@ else:
```py
from b import f
# TODO: We should disambiguate in such cases, showing `Literal[b.f, c.f]`.
reveal_type(f) # revealed: Literal[f, f]
# TODO: We should disambiguate in such cases between `b.f` and `c.f`.
reveal_type(f) # revealed: (def f() -> Unknown) | (def f() -> Unknown)
```
## Reimport with stub declaration

View File

@@ -122,6 +122,11 @@ from c import Y # error: [unresolved-import]
## Esoteric definitions and redefinintions
```toml
[environment]
python-version = "3.12"
```
We understand all public symbols defined in an external module as being imported by a `*` import,
not just those that are defined in `StmtAssign` nodes and `StmtAnnAssign` nodes. This section
provides tests for definitions, and redefinitions, that use more esoteric AST nodes.
@@ -626,6 +631,30 @@ reveal_type(X) # revealed: Unknown
reveal_type(Y) # revealed: bool
```
### An implicit import in a `.pyi` file later overridden by another assignment
`a.pyi`:
```pyi
X: bool = True
```
`b.pyi`:
```pyi
from a import X
X: bool = False
```
`c.py`:
```py
from b import *
reveal_type(X) # revealed: bool
```
## Visibility constraints
If an `importer` module contains a `from exporter import *` statement in its global namespace, the
@@ -865,15 +894,10 @@ from exporter import *
reveal_type(X) # revealed: bool
# TODO none of these should error, should all reveal `bool`
# error: [unresolved-reference]
reveal_type(_private) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(__protected) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(__dunder__) # revealed: Unknown
# error: [unresolved-reference]
reveal_type(___thunder___) # revealed: Unknown
reveal_type(_private) # revealed: bool
reveal_type(__protected) # revealed: bool
reveal_type(__dunder__) # revealed: bool
reveal_type(___thunder___) # revealed: bool
# TODO: should emit [unresolved-reference] diagnostic & reveal `Unknown`
reveal_type(Y) # revealed: bool
@@ -1034,8 +1058,8 @@ from exporter import *
# At runtime, `f` is imported but `g` is not; to avoid false positives, however,
# we treat `a` as though it does not have `__all__` at all,
# which would imply that both symbols would be present.
reveal_type(f) # revealed: Literal[f]
reveal_type(g) # revealed: Literal[g]
reveal_type(f) # revealed: def f() -> str
reveal_type(g) # revealed: def g() -> int
```
### `__all__` conditionally defined in a statically known branch
@@ -1072,6 +1096,44 @@ reveal_type(Y) # revealed: bool
reveal_type(Z) # revealed: Unknown
```
### `__all__` conditionally defined in a statically known branch (2)
The same example again, but with a different `python-version` set:
```toml
[environment]
python-version = "3.10"
```
`exporter.py`:
```py
import sys
X: bool = True
if sys.version_info >= (3, 11):
__all__ = ["X", "Y"]
Y: bool = True
else:
__all__ = ("Z",)
Z: bool = True
```
`importer.py`:
```py
from exporter import *
# TODO: should reveal `Unknown` and emit `[unresolved-reference]`
reveal_type(X) # revealed: bool
# error: [unresolved-reference]
reveal_type(Y) # revealed: Unknown
reveal_type(Z) # revealed: bool
```
### `__all__` conditionally mutated in a statically known branch
```toml
@@ -1084,11 +1146,11 @@ python-version = "3.11"
```py
import sys
__all__ = ["X"]
__all__ = []
X: bool = True
if sys.version_info >= (3, 11):
__all__.append("Y")
__all__.extend(["X", "Y"])
Y: bool = True
else:
__all__.append("Z")
@@ -1107,6 +1169,45 @@ reveal_type(Y) # revealed: bool
reveal_type(Z) # revealed: Unknown
```
### `__all__` conditionally mutated in a statically known branch (2)
The same example again, but with a different `python-version` set:
```toml
[environment]
python-version = "3.10"
```
`exporter.py`:
```py
import sys
__all__ = []
X: bool = True
if sys.version_info >= (3, 11):
__all__.extend(["X", "Y"])
Y: bool = True
else:
__all__.append("Z")
Z: bool = True
```
`importer.py`:
```py
from exporter import *
# TODO: should reveal `Unknown` & emit `[unresolved-reference]
reveal_type(X) # revealed: bool
# error: [unresolved-reference]
reveal_type(Y) # revealed: Unknown
reveal_type(Z) # revealed: bool
```
### Empty `__all__`
An empty `__all__` is valid, but a `*` import from a module with an empty `__all__` results in 0
@@ -1166,6 +1267,7 @@ from b import *
# TODO: should not error, should reveal `bool`
# (`X` is re-exported from `b.pyi` due to presence in `__all__`)
# See https://github.com/astral-sh/ruff/issues/16159
#
# error: [unresolved-reference]
reveal_type(X) # revealed: Unknown
@@ -1198,7 +1300,7 @@ f()
```py
from a import *
reveal_type(f) # revealed: Literal[f]
reveal_type(f) # revealed: def f() -> Unknown
# TODO: we're undecided about whether we should consider this a false positive or not.
# Mutating the global scope to add a symbol from an inner scope will not *necessarily* result

View File

@@ -842,7 +842,7 @@ def unknown(
### Mixed dynamic types
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
Gradually-equivalent types can be simplified out of intersections:
```py
from typing import Any
@@ -854,10 +854,10 @@ def mixed(
i3: Intersection[Not[Any], Unknown],
i4: Intersection[Not[Any], Not[Unknown]],
) -> None:
reveal_type(i1) # revealed: Any & Unknown
reveal_type(i2) # revealed: Any & Unknown
reveal_type(i3) # revealed: Any & Unknown
reveal_type(i4) # revealed: Any & Unknown
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: Any
reveal_type(i3) # revealed: Any
reveal_type(i4) # revealed: Any
```
## Invalid

View File

@@ -321,7 +321,7 @@ def _(flag: bool):
# TODO... `int` might be ideal here?
reveal_type(x) # revealed: int | Unknown
# error: [not-iterable] "Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `<bound method `__iter__` of `Iterable2`> | None`) may not be callable"
# error: [not-iterable] "Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable"
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown

View File

@@ -216,6 +216,11 @@ reveal_type(A.__class__) # revealed: type[Unknown]
## PEP 695 generic
```toml
[environment]
python-version = "3.12"
```
```py
class M(type): ...
class A[T: str](metaclass=M): ...

View File

@@ -0,0 +1,53 @@
# Narrowing with assert statements
## `assert` a value `is None` or `is not None`
```py
def _(x: str | None, y: str | None):
assert x is not None
reveal_type(x) # revealed: str
assert y is None
reveal_type(y) # revealed: None
```
## `assert` a value is truthy or falsy
```py
def _(x: bool, y: bool):
assert x
reveal_type(x) # revealed: Literal[True]
assert not y
reveal_type(y) # revealed: Literal[False]
```
## `assert` with `is` and `==` for literals
```py
from typing import Literal
def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]):
assert x is 2
reveal_type(x) # revealed: Literal[2]
assert y == 2
reveal_type(y) # revealed: Literal[1, 2, 3]
```
## `assert` with `isinstance`
```py
def _(x: int | str):
assert isinstance(x, int)
reveal_type(x) # revealed: int
```
## `assert` a value `in` a tuple
```py
from typing import Literal
def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]):
assert x in (1, 2)
reveal_type(x) # revealed: Literal[1, 2]
assert y not in (1, 2)
reveal_type(y) # revealed: Literal[3]
```

View File

@@ -223,3 +223,15 @@ def _(x: str | None, y: str | None):
if y is not x:
reveal_type(y) # revealed: str | None
```
## Assignment expressions
```py
def f() -> bool:
return True
if x := f():
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
```

View File

@@ -47,3 +47,16 @@ def _(flag1: bool, flag2: bool):
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2]
```
## Assignment expressions
```py
def f() -> int | str | None: ...
if isinstance(x := f(), int):
reveal_type(x) # revealed: int
elif isinstance(x, str):
reveal_type(x) # revealed: str & ~int
else:
reveal_type(x) # revealed: None
```

View File

@@ -78,3 +78,17 @@ def _(x: Literal[1, "a", "b", "c", "d"]):
else:
reveal_type(x) # revealed: Literal[1, "d"]
```
## Assignment expressions
```py
from typing import Literal
def f() -> Literal[1, 2, 3]:
return 1
if (x := f()) in (1,):
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Literal[2, 3]
```

View File

@@ -100,3 +100,16 @@ def _(flag: bool):
else:
reveal_type(x) # revealed: Literal[42]
```
## Assignment expressions
```py
from typing import Literal
def f() -> Literal[1, 2] | None: ...
if (x := f()) is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1, 2]
```

View File

@@ -82,3 +82,14 @@ def _(x_flag: bool, y_flag: bool):
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
```
## Assignment expressions
```py
def f() -> int | str | None: ...
if (x := f()) is not None:
reveal_type(x) # revealed: int | str
else:
reveal_type(x) # revealed: None
```

View File

@@ -89,3 +89,18 @@ def _(flag1: bool, flag2: bool, a: int):
else:
reveal_type(x) # revealed: Literal[1, 2]
```
## Assignment expressions
```py
from typing import Literal
def f() -> Literal[1, 2, 3]:
return 1
if (x := f()) != 1:
reveal_type(x) # revealed: Literal[2, 3]
else:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
```

View File

@@ -1,5 +1,10 @@
# Narrowing for `match` statements
```toml
[environment]
python-version = "3.10"
```
## Single `match` pattern
```py
@@ -34,8 +39,7 @@ match x:
case A():
reveal_type(x) # revealed: A
case B():
# TODO could be `B & ~A`
reveal_type(x) # revealed: B
reveal_type(x) # revealed: B & ~A
reveal_type(x) # revealed: object
```
@@ -83,7 +87,7 @@ match x:
case 6.0:
reveal_type(x) # revealed: float
case 1j:
reveal_type(x) # revealed: complex
reveal_type(x) # revealed: complex & ~float
case b"foo":
reveal_type(x) # revealed: Literal[b"foo"]
@@ -129,11 +133,11 @@ match x:
case "foo" | 42 | None:
reveal_type(x) # revealed: Literal["foo", 42] | None
case "foo" | tuple():
reveal_type(x) # revealed: Literal["foo"] | tuple
reveal_type(x) # revealed: tuple
case True | False:
reveal_type(x) # revealed: bool
case 3.14 | 2.718 | 1.414:
reveal_type(x) # revealed: float
reveal_type(x) # revealed: float & ~tuple
reveal_type(x) # revealed: object
```
@@ -160,3 +164,49 @@ match x:
reveal_type(x) # revealed: object
```
## Narrowing due to guard
```py
def get_object() -> object:
return object()
x = get_object()
reveal_type(x) # revealed: object
match x:
case str() | float() if type(x) is str:
reveal_type(x) # revealed: str
case "foo" | 42 | None if isinstance(x, int):
reveal_type(x) # revealed: Literal[42]
case False if x:
reveal_type(x) # revealed: Never
case "foo" if x := "bar":
reveal_type(x) # revealed: Literal["bar"]
reveal_type(x) # revealed: object
```
## Guard and reveal_type in guard
```py
def get_object() -> object:
return object()
x = get_object()
reveal_type(x) # revealed: object
match x:
case str() | float() if type(x) is str and reveal_type(x): # revealed: str
pass
case "foo" | 42 | None if isinstance(x, int) and reveal_type(x): # revealed: Literal[42]
pass
case False if x and reveal_type(x): # revealed: Never
pass
case "foo" if (x := "bar") and reveal_type(x): # revealed: Literal["bar"]
pass
reveal_type(x) # revealed: object
```

View File

@@ -63,7 +63,7 @@ def bar(world: str, *args, **kwargs) -> float:
x = foo if flag() else bar
if x:
reveal_type(x) # revealed: Literal[foo, bar]
reveal_type(x) # revealed: (def foo(hello: int) -> bytes) | (def bar(world: str, *args, **kwargs) -> int | float)
else:
reveal_type(x) # revealed: Never
```
@@ -246,7 +246,7 @@ class MetaTruthy(type):
class MetaDeferred(type):
def __bool__(self) -> MetaAmbiguous:
return MetaAmbiguous()
raise NotImplementedError
class AmbiguousClass(metaclass=MetaAmbiguous): ...
class FalsyClass(metaclass=MetaFalsy): ...

View File

@@ -111,6 +111,11 @@ def _(x: A | B):
## Narrowing for generic classes
```toml
[environment]
python-version = "3.13"
```
Note that `type` returns the runtime class of an object, which does _not_ include specializations in
the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the
specialization that we compare with; we must narrow to an unknown specialization of the generic
@@ -139,3 +144,13 @@ def _(x: Base):
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
reveal_type(x) # revealed: Base
```
## Assignment expressions
```py
def _(x: object):
if (y := type(x)) is bool:
reveal_type(y) # revealed: Literal[bool]
if (type(y := x)) is bool:
reveal_type(y) # revealed: bool
```

View File

@@ -0,0 +1,638 @@
# Overloads
Reference: <https://typing.python.org/en/latest/spec/overload.html>
## `typing.overload`
The definition of `typing.overload` in typeshed is an identity function.
```py
from typing import overload
def foo(x: int) -> int:
return x
reveal_type(foo) # revealed: def foo(x: int) -> int
bar = overload(foo)
reveal_type(bar) # revealed: def foo(x: int) -> int
```
## Functions
```py
from typing import overload
@overload
def add() -> None: ...
@overload
def add(x: int) -> int: ...
@overload
def add(x: int, y: int) -> int: ...
def add(x: int | None = None, y: int | None = None) -> int | None:
return (x or 0) + (y or 0)
reveal_type(add) # revealed: Overload[() -> None, (x: int) -> int, (x: int, y: int) -> int]
reveal_type(add()) # revealed: None
reveal_type(add(1)) # revealed: int
reveal_type(add(1, 2)) # revealed: int
```
## Overriding
These scenarios are to verify that the overloaded and non-overloaded definitions are correctly
overridden by each other.
An overloaded function is overriding another overloaded function:
```py
from typing import overload
@overload
def foo() -> None: ...
@overload
def foo(x: int) -> int: ...
def foo(x: int | None = None) -> int | None:
return x
reveal_type(foo) # revealed: Overload[() -> None, (x: int) -> int]
reveal_type(foo()) # revealed: None
reveal_type(foo(1)) # revealed: int
@overload
def foo() -> None: ...
@overload
def foo(x: str) -> str: ...
def foo(x: str | None = None) -> str | None:
return x
reveal_type(foo) # revealed: Overload[() -> None, (x: str) -> str]
reveal_type(foo()) # revealed: None
reveal_type(foo("")) # revealed: str
```
A non-overloaded function is overriding an overloaded function:
```py
def foo(x: int) -> int:
return x
reveal_type(foo) # revealed: def foo(x: int) -> int
```
An overloaded function is overriding a non-overloaded function:
```py
reveal_type(foo) # revealed: def foo(x: int) -> int
@overload
def foo() -> None: ...
@overload
def foo(x: bytes) -> bytes: ...
def foo(x: bytes | None = None) -> bytes | None:
return x
reveal_type(foo) # revealed: Overload[() -> None, (x: bytes) -> bytes]
reveal_type(foo()) # revealed: None
reveal_type(foo(b"")) # revealed: bytes
```
## Methods
```py
from typing import overload
class Foo1:
@overload
def method(self) -> None: ...
@overload
def method(self, x: int) -> int: ...
def method(self, x: int | None = None) -> int | None:
return x
foo1 = Foo1()
reveal_type(foo1.method) # revealed: Overload[() -> None, (x: int) -> int]
reveal_type(foo1.method()) # revealed: None
reveal_type(foo1.method(1)) # revealed: int
class Foo2:
@overload
def method(self) -> None: ...
@overload
def method(self, x: str) -> str: ...
def method(self, x: str | None = None) -> str | None:
return x
foo2 = Foo2()
reveal_type(foo2.method) # revealed: Overload[() -> None, (x: str) -> str]
reveal_type(foo2.method()) # revealed: None
reveal_type(foo2.method("")) # revealed: str
```
## Constructor
```py
from typing import overload
class Foo:
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: int) -> None: ...
def __init__(self, x: int | None = None) -> None:
self.x = x
foo = Foo()
reveal_type(foo) # revealed: Foo
reveal_type(foo.x) # revealed: Unknown | int | None
foo1 = Foo(1)
reveal_type(foo1) # revealed: Foo
reveal_type(foo1.x) # revealed: Unknown | int | None
```
## Version specific
Function definitions can vary between multiple Python versions.
### Overload and non-overload (3.9)
Here, the same function is overloaded in one version and not in another.
```toml
[environment]
python-version = "3.9"
```
```py
import sys
from typing import overload
if sys.version_info < (3, 10):
def func(x: int) -> int:
return x
elif sys.version_info <= (3, 12):
@overload
def func() -> None: ...
@overload
def func(x: int) -> int: ...
def func(x: int | None = None) -> int | None:
return x
reveal_type(func) # revealed: def func(x: int) -> int
func() # error: [missing-argument]
```
### Overload and non-overload (3.10)
```toml
[environment]
python-version = "3.10"
```
```py
import sys
from typing import overload
if sys.version_info < (3, 10):
def func(x: int) -> int:
return x
elif sys.version_info <= (3, 12):
@overload
def func() -> None: ...
@overload
def func(x: int) -> int: ...
def func(x: int | None = None) -> int | None:
return x
reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int]
reveal_type(func()) # revealed: None
reveal_type(func(1)) # revealed: int
```
### Some overloads are version specific (3.9)
```toml
[environment]
python-version = "3.9"
```
`overloaded.pyi`:
```pyi
import sys
from typing import overload
if sys.version_info >= (3, 10):
@overload
def func() -> None: ...
@overload
def func(x: int) -> int: ...
@overload
def func(x: str) -> str: ...
```
`main.py`:
```py
from overloaded import func
reveal_type(func) # revealed: Overload[(x: int) -> int, (x: str) -> str]
func() # error: [no-matching-overload]
reveal_type(func(1)) # revealed: int
reveal_type(func("")) # revealed: str
```
### Some overloads are version specific (3.10)
```toml
[environment]
python-version = "3.10"
```
`overloaded.pyi`:
```pyi
import sys
from typing import overload
@overload
def func() -> None: ...
if sys.version_info >= (3, 10):
@overload
def func(x: int) -> int: ...
@overload
def func(x: str) -> str: ...
```
`main.py`:
```py
from overloaded import func
reveal_type(func) # revealed: Overload[() -> None, (x: int) -> int, (x: str) -> str]
reveal_type(func()) # revealed: None
reveal_type(func(1)) # revealed: int
reveal_type(func("")) # revealed: str
```
## Generic
```toml
[environment]
python-version = "3.12"
```
For an overloaded generic function, it's not necessary for all overloads to be generic.
```py
from typing import overload
@overload
def func() -> None: ...
@overload
def func[T](x: T) -> T: ...
def func[T](x: T | None = None) -> T | None:
return x
reveal_type(func) # revealed: Overload[() -> None, (x: T) -> T]
reveal_type(func()) # revealed: None
reveal_type(func(1)) # revealed: Literal[1]
reveal_type(func("")) # revealed: Literal[""]
```
## Invalid
### At least two overloads
At least two `@overload`-decorated definitions must be present.
```py
from typing import overload
# TODO: error
@overload
def func(x: int) -> int: ...
def func(x: int | str) -> int | str:
return x
```
### Overload without an implementation
#### Regular modules
In regular modules, a series of `@overload`-decorated definitions must be followed by exactly one
non-`@overload`-decorated definition (for the same function/method).
```py
from typing import overload
# TODO: error because implementation does not exists
@overload
def func(x: int) -> int: ...
@overload
def func(x: str) -> str: ...
class Foo:
# TODO: error because implementation does not exists
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
```
#### Stub files
Overload definitions within stub files are exempt from this check.
```pyi
from typing import overload
@overload
def func(x: int) -> int: ...
@overload
def func(x: str) -> str: ...
```
#### Protocols
Overload definitions within protocols are exempt from this check.
```py
from typing import Protocol, overload
class Foo(Protocol):
@overload
def f(self, x: int) -> int: ...
@overload
def f(self, x: str) -> str: ...
```
#### Abstract methods
Overload definitions within abstract base classes are exempt from this check.
```py
from abc import ABC, abstractmethod
from typing import overload
class AbstractFoo(ABC):
@overload
@abstractmethod
def f(self, x: int) -> int: ...
@overload
@abstractmethod
def f(self, x: str) -> str: ...
```
Using the `@abstractmethod` decorator requires that the class's metaclass is `ABCMeta` or is derived
from it.
```py
class Foo:
# TODO: Error because implementation does not exists
@overload
@abstractmethod
def f(self, x: int) -> int: ...
@overload
@abstractmethod
def f(self, x: str) -> str: ...
```
And, the `@abstractmethod` decorator must be present on all the `@overload`-ed methods.
```py
class PartialFoo1(ABC):
@overload
@abstractmethod
def f(self, x: int) -> int: ...
@overload
def f(self, x: str) -> str: ...
class PartialFoo(ABC):
@overload
def f(self, x: int) -> int: ...
@overload
@abstractmethod
def f(self, x: str) -> str: ...
```
### Inconsistent decorators
#### `@staticmethod` / `@classmethod`
If one overload signature is decorated with `@staticmethod` or `@classmethod`, all overload
signatures must be similarly decorated. The implementation, if present, must also have a consistent
decorator.
```py
from __future__ import annotations
from typing import overload
class CheckStaticMethod:
# TODO: error because `@staticmethod` does not exist on all overloads
@overload
def method1(x: int) -> int: ...
@overload
def method1(x: str) -> str: ...
@staticmethod
def method1(x: int | str) -> int | str:
return x
# TODO: error because `@staticmethod` does not exist on all overloads
@overload
def method2(x: int) -> int: ...
@overload
@staticmethod
def method2(x: str) -> str: ...
@staticmethod
def method2(x: int | str) -> int | str:
return x
# TODO: error because `@staticmethod` does not exist on the implementation
@overload
@staticmethod
def method3(x: int) -> int: ...
@overload
@staticmethod
def method3(x: str) -> str: ...
def method3(x: int | str) -> int | str:
return x
@overload
@staticmethod
def method4(x: int) -> int: ...
@overload
@staticmethod
def method4(x: str) -> str: ...
@staticmethod
def method4(x: int | str) -> int | str:
return x
class CheckClassMethod:
def __init__(self, x: int) -> None:
self.x = x
# TODO: error because `@classmethod` does not exist on all overloads
@overload
@classmethod
def try_from1(cls, x: int) -> CheckClassMethod: ...
@overload
def try_from1(cls, x: str) -> None: ...
@classmethod
def try_from1(cls, x: int | str) -> CheckClassMethod | None:
if isinstance(x, int):
return cls(x)
return None
# TODO: error because `@classmethod` does not exist on all overloads
@overload
def try_from2(cls, x: int) -> CheckClassMethod: ...
@overload
@classmethod
def try_from2(cls, x: str) -> None: ...
@classmethod
def try_from2(cls, x: int | str) -> CheckClassMethod | None:
if isinstance(x, int):
return cls(x)
return None
# TODO: error because `@classmethod` does not exist on the implementation
@overload
@classmethod
def try_from3(cls, x: int) -> CheckClassMethod: ...
@overload
@classmethod
def try_from3(cls, x: str) -> None: ...
def try_from3(cls, x: int | str) -> CheckClassMethod | None:
if isinstance(x, int):
return cls(x)
return None
@overload
@classmethod
def try_from4(cls, x: int) -> CheckClassMethod: ...
@overload
@classmethod
def try_from4(cls, x: str) -> None: ...
@classmethod
def try_from4(cls, x: int | str) -> CheckClassMethod | None:
if isinstance(x, int):
return cls(x)
return None
```
#### `@final` / `@override`
If a `@final` or `@override` decorator is supplied for a function with overloads, the decorator
should be applied only to the overload implementation if it is present.
```py
from typing_extensions import final, overload, override
class Foo:
@overload
def method1(self, x: int) -> int: ...
@overload
def method1(self, x: str) -> str: ...
@final
def method1(self, x: int | str) -> int | str:
return x
# TODO: error because `@final` is not on the implementation
@overload
@final
def method2(self, x: int) -> int: ...
@overload
def method2(self, x: str) -> str: ...
def method2(self, x: int | str) -> int | str:
return x
# TODO: error because `@final` is not on the implementation
@overload
def method3(self, x: int) -> int: ...
@overload
@final
def method3(self, x: str) -> str: ...
def method3(self, x: int | str) -> int | str:
return x
class Base:
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
def method(self, x: int | str) -> int | str:
return x
class Sub1(Base):
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
@override
def method(self, x: int | str) -> int | str:
return x
class Sub2(Base):
# TODO: error because `@override` is not on the implementation
@overload
def method(self, x: int) -> int: ...
@overload
@override
def method(self, x: str) -> str: ...
def method(self, x: int | str) -> int | str:
return x
class Sub3(Base):
# TODO: error because `@override` is not on the implementation
@overload
@override
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
def method(self, x: int | str) -> int | str:
return x
```
#### `@final` / `@override` in stub files
If an overload implementation isnt present (for example, in a stub file), the `@final` or
`@override` decorator should be applied only to the first overload.
```pyi
from typing_extensions import final, overload, override
class Foo:
@overload
@final
def method1(self, x: int) -> int: ...
@overload
def method1(self, x: str) -> str: ...
# TODO: error because `@final` is not on the first overload
@overload
def method2(self, x: int) -> int: ...
@final
@overload
def method2(self, x: str) -> str: ...
class Base:
@overload
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
class Sub1(Base):
@overload
@override
def method(self, x: int) -> int: ...
@overload
def method(self, x: str) -> str: ...
class Sub2(Base):
# TODO: error because `@override` is not on the first overload
@overload
def method(self, x: int) -> int: ...
@overload
@override
def method(self, x: str) -> str: ...
```

View File

@@ -146,7 +146,7 @@ class C:
@property
def attr(self) -> int:
return 1
# error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 2 (`fset`) of bound method `setter`; expected type `(Any, Any, /) -> None`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `(Any, Any, /) -> None`, found `def attr(self) -> None`"
@attr.setter
def attr(self) -> None:
pass
@@ -156,7 +156,7 @@ class C:
```py
class C:
# error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 1 (`fget`) of class `property`; expected type `((Any, /) -> Any) | None`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `((Any, /) -> Any) | None`, found `def attr(self, x: int) -> int`"
@property
def attr(self, x: int) -> int:
return 1
@@ -294,10 +294,10 @@ Properties also have `fget` and `fset` attributes that can be used to retrieve t
and setter functions, respectively.
```py
reveal_type(attr_property.fget) # revealed: Literal[attr]
reveal_type(attr_property.fget) # revealed: def attr(self) -> int
reveal_type(attr_property.fget(c)) # revealed: int
reveal_type(attr_property.fset) # revealed: Literal[attr]
reveal_type(attr_property.fset) # revealed: def attr(self, value: str) -> None
reveal_type(attr_property.fset(c, "a")) # revealed: None
# error: [invalid-argument-type]

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ if returns_bool():
chr: int = 1
def f():
reveal_type(chr) # revealed: int | Literal[chr]
reveal_type(chr) # revealed: int | (def chr(i: int | SupportsIndex, /) -> str)
```
## Conditionally global or builtin, with annotation
@@ -28,5 +28,5 @@ if returns_bool():
chr: int = 1
def f():
reveal_type(chr) # revealed: int | Literal[chr]
reveal_type(chr) # revealed: int | (def chr(i: int | SupportsIndex, /) -> str)
```

View File

@@ -14,7 +14,7 @@ reveal_type(__package__) # revealed: str | None
reveal_type(__doc__) # revealed: str | None
reveal_type(__spec__) # revealed: ModuleSpec | None
reveal_type(__path__) # revealed: @Todo(generics)
reveal_type(__path__) # revealed: @Todo(specialized non-generic class)
class X:
reveal_type(__name__) # revealed: str
@@ -51,15 +51,15 @@ inside the module:
import typing
reveal_type(typing.__name__) # revealed: str
reveal_type(typing.__init__) # revealed: <bound method `__init__` of `ModuleType`>
reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ellipsis) -> None
# These come from `builtins.object`, not `types.ModuleType`:
reveal_type(typing.__eq__) # revealed: <bound method `__eq__` of `ModuleType`>
reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) -> bool
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
# TODO: needs support generics; should be `dict[str, Any]`:
reveal_type(typing.__dict__) # revealed: @Todo(generics)
reveal_type(typing.__dict__) # revealed: @Todo(specialized non-generic class)
```
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
@@ -92,8 +92,8 @@ import foo
from foo import __dict__ as foo_dict
# TODO: needs support generics; should be `dict[str, Any]` for both of these:
reveal_type(foo.__dict__) # revealed: @Todo(generics)
reveal_type(foo_dict) # revealed: @Todo(generics)
reveal_type(foo.__dict__) # revealed: @Todo(specialized non-generic class)
reveal_type(foo_dict) # revealed: @Todo(specialized non-generic class)
```
## Conditionally global or `ModuleType` attribute

View File

@@ -37,17 +37,17 @@ reveal_type(f) # revealed: Literal[1]
def f(): ...
reveal_type(f) # revealed: Literal[f]
reveal_type(f) # revealed: def f() -> Unknown
def f(x: int) -> int:
raise NotImplementedError
reveal_type(f) # revealed: Literal[f]
reveal_type(f) # revealed: def f(x: int) -> int
f: int = 1
reveal_type(f) # revealed: Literal[1]
def f(): ...
reveal_type(f) # revealed: Literal[f]
reveal_type(f) # revealed: def f() -> Unknown
```

View File

@@ -40,13 +40,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:11:5
|
9 | # error: [not-iterable]
10 | for x in Iterable():
11 | reveal_type(x) # revealed: int
| ^^^^^^^^^^^^^^ Revealed type is `int`
| ^^^^^^^^^^^^^^ `int`
|
```

View File

@@ -37,13 +37,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:8:5
|
6 | # error: [not-iterable]
7 | for x in Bad():
8 | reveal_type(x) # revealed: Unknown
| ^^^^^^^^^^^^^^ Revealed type is `Unknown`
| ^^^^^^^^^^^^^^ `Unknown`
|
```

View File

@@ -59,13 +59,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:24:9
|
22 | for x in Iterable1():
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
| ^^^^^^^^^^^^^^ Revealed type is `int | Unknown`
| ^^^^^^^^^^^^^^ `int | Unknown`
25 |
26 | # error: [not-iterable]
|
@@ -78,7 +78,7 @@ error: lint:not-iterable
|
26 | # error: [not-iterable]
27 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable2`> | None`) may not be callable
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
|
@@ -86,13 +86,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:29:9
|
27 | for y in Iterable2():
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
| ^^^^^^^^^^^^^^ Revealed type is `int | Unknown`
| ^^^^^^^^^^^^^^ `int | Unknown`
|
```

View File

@@ -48,7 +48,7 @@ error: lint:not-iterable
|
19 | # error: [not-iterable]
20 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable1`> | None`) may not be callable
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
|
@@ -56,13 +56,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:22:9
|
20 | for x in Iterable1():
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
| ^^^^^^^^^^^^^^ Revealed type is `str | Unknown`
| ^^^^^^^^^^^^^^ `str | Unknown`
23 |
24 | # error: [not-iterable]
|
@@ -75,20 +75,20 @@ error: lint:not-iterable
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `<bound method `__getitem__` of `Iterable2`> | <bound method `__getitem__` of `Iterable2`>`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
26 | reveal_type(y) # revealed: str | int
|
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:26:9
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
26 | reveal_type(y) # revealed: str | int
| ^^^^^^^^^^^^^^ Revealed type is `str | int`
| ^^^^^^^^^^^^^^ `str | int`
|
```

View File

@@ -52,20 +52,20 @@ error: lint:not-iterable
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `<bound method `__iter__` of `Iterable1`> | <bound method `__iter__` of `Iterable1`>`) may have an invalid signature (expected `def __iter__(self): ...`)
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`)
18 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:18:9
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
18 | reveal_type(x) # revealed: int
| ^^^^^^^^^^^^^^ Revealed type is `int`
| ^^^^^^^^^^^^^^ `int`
19 |
20 | class Iterable2:
|
@@ -78,7 +78,7 @@ error: lint:not-iterable
|
27 | # error: [not-iterable]
28 | for x in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `<bound method `__iter__` of `Iterable2`> | None`) may not be callable
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
|
@@ -86,13 +86,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:30:9
|
28 | for x in Iterable2():
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
| ^^^^^^^^^^^^^^ Revealed type is `int | Unknown`
| ^^^^^^^^^^^^^^ `int | Unknown`
|
```

View File

@@ -63,13 +63,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:29:9
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
29 | reveal_type(x) # revealed: int | str
| ^^^^^^^^^^^^^^ Revealed type is `int | str`
| ^^^^^^^^^^^^^^ `int | str`
30 |
31 | # error: [not-iterable]
|
@@ -90,13 +90,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:34:9
|
32 | for y in Iterable2():
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
| ^^^^^^^^^^^^^^ Revealed type is `int | Unknown`
| ^^^^^^^^^^^^^^ `int | Unknown`
|
```

View File

@@ -48,13 +48,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:19:9
|
17 | # error: [not-iterable]
18 | for x in Iterable():
19 | reveal_type(x) # revealed: int | bytes
| ^^^^^^^^^^^^^^ Revealed type is `int | bytes`
| ^^^^^^^^^^^^^^ `int | bytes`
|
```

View File

@@ -59,7 +59,7 @@ error: lint:not-iterable
|
30 | # error: [not-iterable]
31 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable1`> | None`) may not be callable
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
@@ -67,13 +67,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:33:9
|
31 | for x in Iterable1():
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
| ^^^^^^^^^^^^^^ Revealed type is `bytes | str | Unknown`
| ^^^^^^^^^^^^^^ `bytes | str | Unknown`
34 |
35 | # error: [not-iterable]
|
@@ -86,20 +86,20 @@ error: lint:not-iterable
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `<bound method `__getitem__` of `Iterable2`> | <bound method `__getitem__` of `Iterable2`>`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
37 | reveal_type(y) # revealed: bytes | str | int
|
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:37:9
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
37 | reveal_type(y) # revealed: bytes | str | int
| ^^^^^^^^^^^^^^ Revealed type is `bytes | str | int`
| ^^^^^^^^^^^^^^ `bytes | str | int`
|
```

View File

@@ -47,13 +47,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:18:9
|
16 | # error: [not-iterable]
17 | for x in Iterable():
18 | reveal_type(x) # revealed: int | bytes
| ^^^^^^^^^^^^^^ Revealed type is `int | bytes`
| ^^^^^^^^^^^^^^ `int | bytes`
|
```

View File

@@ -49,13 +49,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:19:9
|
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
19 | reveal_type(x) # revealed: int
| ^^^^^^^^^^^^^^ Revealed type is `int`
| ^^^^^^^^^^^^^^ `int`
|
```

View File

@@ -44,13 +44,13 @@ error: lint:not-iterable
```
```
info: revealed-type
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:14:9
|
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
14 | reveal_type(x) # revealed: int
| ^^^^^^^^^^^^^^ Revealed type is `int`
| ^^^^^^^^^^^^^^ `int`
|
```

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