Compare commits

...

128 Commits

Author SHA1 Message Date
Dhruv Manilawala
9ea31a8923 TypeVarInstance and intersection of callables 2025-04-28 16:27:17 -05:00
Wei Lee
504fa20057 [airflow] Apply auto fixes to cases where the names have changed in Airflow 3 (AIR302) (#17553)
## Summary

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

Apply auto fixes to cases where the names have changed in Airflow 3 in
AIR302 and split the huge test cases into different test cases based on
proivder

## Test Plan

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

the test cases has been split into multiple for easier checking
2025-04-28 16:35:17 -04:00
David Peter
f0868ac0c9 [red-knot] Revert blanket clippy::too_many_arguments allow (#17688)
## Summary

Now that https://github.com/salsa-rs/salsa/issues/808 has been fixed, we
can revert this global change in `Cargo.toml`.
2025-04-28 21:21:53 +02:00
Brent Westbrook
01a31c08f5 Add config option to disable typing_extensions imports (#17611)
Summary
--

This PR resolves https://github.com/astral-sh/ruff/issues/9761 by adding
a linter configuration option to disable
`typing_extensions` imports. As mentioned [here], it would be ideal if
we could
detect whether or not `typing_extensions` is available as a dependency
automatically, but this seems like a much easier fix in the meantime.

The default for the new option, `typing-extensions`, is `true`,
preserving the current behavior. Setting it to `false` will bail out of
the new
`Checker::typing_importer` method, which has been refactored from the 
`Checker::import_from_typing` method in
https://github.com/astral-sh/ruff/pull/17340),
with `None`, which is then handled specially by each rule that calls it.

I considered some alternatives to a config option, such as checking if
`typing_extensions` has been imported or checking for a `TYPE_CHECKING`
block we could use, but I think defaulting to allowing
`typing_extensions` imports and allowing the user to disable this with
an option is both simple to implement and pretty intuitive.

[here]:
https://github.com/astral-sh/ruff/issues/9761#issuecomment-2790492853

Test Plan
--

New linter tests exercising several combinations of Python versions and
the new config option for PYI019. I also added tests for the other
affected rules, but only in the case where the new config option is
enabled. The rules' existing tests also cover the default case.
2025-04-28 14:57:36 -04:00
Andrew Gallant
405878a128 ruff_db: render file paths in diagnostics as relative paths if possible
This is done in what appears to be the same way as Ruff: we get the CWD,
strip the prefix from the path if possible, and use that. If stripping
the prefix fails, then we print the full path as-is.

Fixes #17233
2025-04-28 14:32:34 -04:00
Alex Waygood
80103a179d Bump mypy_primer pin (#17685) 2025-04-28 16:13:07 +00:00
Andrew Gallant
9a8f3cf247 red_knot_python_semantic: improve not-iterable diagnostic
This cleans up one particular TODO by splitting the "because" part of
the `not-iterable` diagnostic out into an info sub-diagnostic.
2025-04-28 11:03:41 -04:00
David Peter
07718f4788 [red-knot] Allow all callables to be assignable to @Todo-signatures (#17680)
## Summary

Removes ~850 diagnostics related to assignability of callable types,
where the callable-being-assigned-to has a "Todo signature", which
should probably accept any left hand side callable/signature.
2025-04-28 16:40:35 +02:00
Dylan
1e8881f9af [refurb] Mark fix as safe for readlines-in-for (FURB129) (#17644)
This PR promotes the fix applicability of [readlines-in-for
(FURB129)](https://docs.astral.sh/ruff/rules/readlines-in-for/#readlines-in-for-furb129)
to always safe.

In the original PR (https://github.com/astral-sh/ruff/pull/9880), the
author marked the rule as unsafe because Ruff's type inference couldn't
quite guarantee that we had an `IOBase` object in hand. Some false
positives were recorded in the test fixture. However, before the PR was
merged, Charlie added the necessary type inference and the false
positives went away.

According to the [Python
documentation](https://docs.python.org/3/library/io.html#io.IOBase), I
believe this fix is safe for any proper implementation of `IOBase`:

>[IOBase](https://docs.python.org/3/library/io.html#io.IOBase) (and its
subclasses) supports the iterator protocol, meaning that an
[IOBase](https://docs.python.org/3/library/io.html#io.IOBase) object can
be iterated over yielding the lines in a stream. Lines are defined
slightly differently depending on whether the stream is a binary stream
(yielding bytes), or a text stream (yielding character strings). See
[readline()](https://docs.python.org/3/library/io.html#io.IOBase.readline)
below.

and then in the [documentation for
`readlines`](https://docs.python.org/3/library/io.html#io.IOBase.readlines):

>Read and return a list of lines from the stream. hint can be specified
to control the number of lines read: no more lines will be read if the
total size (in bytes/characters) of all lines so far exceeds hint. [...]
>Note that it’s already possible to iterate on file objects using for
line in file: ... without calling file.readlines().

I believe that a careful reading of our [versioning
policy](https://docs.astral.sh/ruff/versioning/#version-changes)
requires that this change be deferred to a minor release - but please
correct me if I'm wrong!
2025-04-28 09:39:55 -05:00
Dylan
152a0b6585 Collect preview lint behaviors in separate module (#17646)
This PR collects all behavior gated under preview into a new module
`ruff_linter::preview` that exposes functions like
`is_my_new_feature_enabled` - just as is done in the formatter crate.
2025-04-28 09:12:24 -05:00
Alex Waygood
1ad5015e19 Upgrade Salsa to a more recent commit (#17678) 2025-04-28 13:32:19 +01:00
David Peter
92f95ff494 [red-knot] TypedDict: No errors for introspection dunder attributes (#17677)
## Summary

Do not emit errors when accessing introspection dunder attributes such
as `__required_keys__` on `TypedDict`s.
2025-04-28 13:28:43 +02:00
Victor Hugo Gomes
ceb2bf1168 [flake8-pyi] Ensure Literal[None,] | Literal[None,] is not autofixed to None | None (PYI061) (#17659)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-28 12:23:29 +01:00
David Peter
f521358033 [red-knot] No errors for definitions of TypedDicts (#17674)
## Summary

Do not emit errors when defining `TypedDict`s:

```py
from typing_extensions import TypedDict

# No error here
class Person(TypedDict):
    name: str
    age: int | None

# No error for this alternative syntax
Message = TypedDict("Message", {"id": int, "content": str})
```

## Ecosystem analysis

* Removes ~ 450 false positives for `TypedDict` definitions.
* Changes a few diagnostic messages.
* Adds a few (< 10) false positives, for example:
  ```diff
+ error[lint:unresolved-attribute]
/tmp/mypy_primer/projects/hydra-zen/src/hydra_zen/structured_configs/_utils.py:262:5:
Type `Literal[DataclassOptions]` has no attribute `__required_keys__`
+ error[lint:unresolved-attribute]
/tmp/mypy_primer/projects/hydra-zen/src/hydra_zen/structured_configs/_utils.py:262:42:
Type `Literal[DataclassOptions]` has no attribute `__optional_keys__`
  ```
* New true positive

4f8263cd7f/corporate/lib/remote_billing_util.py (L155-L157)
  ```diff
+ error[lint:invalid-assignment]
/tmp/mypy_primer/projects/zulip/corporate/lib/remote_billing_util.py:155:5:
Object of type `RemoteBillingIdentityDict | LegacyServerIdentityDict |
None` is not assignable to `LegacyServerIdentityDict | None`
  ```

## Test Plan

New Markdown tests
2025-04-28 13:13:28 +02:00
renovate[bot]
74081032d9 Update actions/download-artifact digest to d3f86a1 (#17664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-28 10:51:59 +00:00
Micha Reiser
dbc137c951 [red-knot] Use 101 exit code when there's at least one diagnostic with severity 'fatal' (#17640) 2025-04-28 10:03:14 +02:00
Victor Hugo Gomes
826b2c9ff3 [pycodestyle] Fix duplicated diagnostic in E712 (#17651) 2025-04-28 08:31:16 +01:00
jie211
a3e55cfd8f [airflow] fix typos AIR312 (#17673) 2025-04-28 08:31:41 +02:00
justin
d2246278e6 [red-knot] Don't ignore hidden files by default (#17655) 2025-04-28 08:21:11 +02:00
renovate[bot]
6bd1863bf0 Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.7 (#17670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 08:16:10 +02:00
renovate[bot]
97dc58fc77 Update docker/build-push-action digest to 14487ce (#17665)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 08:15:54 +02:00
renovate[bot]
53a9448fb5 Update taiki-e/install-action digest to ab3728c (#17666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 08:15:01 +02:00
renovate[bot]
516291b693 Update dependency react-resizable-panels to v2.1.9 (#17667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 08:10:24 +02:00
renovate[bot]
b09f00a4ef Update dependency ruff to v0.11.7 (#17668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 08:09:50 +02:00
renovate[bot]
03065c245c Update dependency smol-toml to v1.3.4 (#17669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 08:09:33 +02:00
renovate[bot]
b45598389d Update Rust crate jiff to v0.2.10 (#17671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 08:09:10 +02:00
renovate[bot]
4729ff2bc8 Update Rust crate syn to v2.0.101 (#17672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 08:08:41 +02:00
Micha Reiser
1bdb22c139 [red-knot] Fix offset handling in playground for 2-code-point UTF16 characters (#17520) 2025-04-27 11:44:55 +01:00
Micha Reiser
1c65e0ad25 Split SourceLocation into LineColumn and SourceLocation (#17587) 2025-04-27 11:27:33 +01:00
justin
4443f6653c [red-knot] Add --respect-ignore-files flag (#17645)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-04-27 10:55:41 +01:00
Vasco Schiavo
b0d475f353 [ruff] add fix safety section (RUF027) (#17485)
The PR add the `fix safety` section for rule `RUF027` (#15584 ).

Actually, I have an example of a false positive. Should I include it in
the` fix safety` section?

---------

Co-authored-by: Dylan <dylwil3@gmail.com>
2025-04-26 16:43:53 -05:00
Vasco Schiavo
b578a828ef [ruff] add fix safety section (RUF005) (#17484)
The PR add the `fix safety` section for rule `RUF005` (#15584 ).

---------

Co-authored-by: Dylan <dylwil3@gmail.com>
2025-04-26 16:43:02 -05:00
Vasco Schiavo
64ba39a385 [flynt] add fix safety section (FLY002) (#17496)
The PR add the fix safety section for rule `FLY002` (#15584 )

The motivation for the content of the fix safety section is given by the
following example

```python
foo = 1
bar = [2, 3]

try:
    result_join = " ".join((foo, bar))
    print(f"Join result: {result_join}")
except TypeError as e:
    print(f"Join error: {e}")
```

which print `Join error: sequence item 0: expected str instance, int
found`

But after the fix is applied, we have

```python
foo = 1
bar = [2, 3]

try:
    result_join = f"{foo} {bar}"
    print(f"Join result: {result_join}")
except TypeError as e:
    print(f"Join error: {e}")
```

which print `Join result: 1 [2, 3]`

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-04-26 16:00:01 +00:00
Hans
a4e225ee8a [flake8-async] Add fix safety section (ASYNC116) (#17497)
## Summary

This PR add the `fix safety` section for rule `ASYNC116` in
`long_sleep_not_forever.rs` for #15584

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-04-26 10:40:51 -05:00
Vasco Schiavo
45d0634b01 [pydocstyle] add fix safety section (D200) (#17502)
The PR add the fix safety section for rule `D200` (#15584 )
2025-04-26 08:59:05 -05:00
Vasco Schiavo
4bcf1778fa [ruff] add fix safety section (RUF057) (#17483)
The PR add the `fix safety` section for rule `RUF057` (#15584 )
2025-04-26 06:58:52 -05:00
Micha Reiser
6044f04137 Revert "[red-knot] Add --respect-ignore-files flag (#17569)" (#17642) 2025-04-26 10:30:50 +00:00
justin
2e95475f57 [red-knot] Add --respect-ignore-files flag (#17569)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-04-26 10:02:03 +00:00
Micha Reiser
cfa1505068 [red-knot] Fix CLI hang when a dependent query panics (#17631) 2025-04-26 06:28:45 +00:00
Dhruv Manilawala
0251679f87 [red-knot] Add new property tests for subtyping with "bottom" callable (#17635)
## Summary

I remember we discussed about adding this as a property tests so here I
am.

## Test Plan

```console
❯ QUICKCHECK_TESTS=10000000 cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable::bottom_callable_is_subtype_of_all_fully_static_callable
    Finished `release` profile [optimized] target(s) in 0.10s
     Running unittests src/lib.rs (target/release/deps/red_knot_python_semantic-e41596ca2dbd0e98)
running 1 test
test types::property_tests::stable::bottom_callable_is_subtype_of_all_fully_static_callable ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 233 filtered out; finished in 30.91s
```
2025-04-26 03:58:13 +05:30
Douglas Creager
6ab32a7746 [red-knot] Create generic context for generic classes lazily (#17617)
As discussed today, this is needed to handle legacy generic classes
without having to infer the types of the class's explicit bases eagerly
at class construction time. Pulling this out into a separate PR so
there's a smaller diff to review.

This also makes our representation of generic classes and functions more
consistent — before, we had separate Rust types and enum variants for
generic/non-generic classes, but a single type for generic functions.
Now we each a single (respective) type for each.

There were very few places we were differentiation between generic and
non-generic _class literals_, and these are handled now by calling the
(salsa cached) `generic_context` _accessor function_.

Note that _`ClassType`_ is still an enum with distinct variants for
non-generic classes and specialized generic classes.
2025-04-25 14:10:03 -04:00
Andrew Gallant
bc0a5aa409 ruff_db: add tests for annotations with no ranges
... and fix the case where an annotation with a `Span` but no
`TextRange` or message gets completely dropped.
2025-04-25 13:25:20 -04:00
Wei Lee
aba21a5d47 [airflow] Extend AIR301 rule (#17598)
## Summary

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

Add "airflow.operators.python.get_current_context" →
"airflow.sdk.get_current_context" rule

## Test Plan

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

the test fixture has been updated accordingly
2025-04-25 12:49:32 -04:00
Wei Lee
b6281a8805 [airflow] update existing AIR302 rules with better suggestions (#17542)
## Summary

Even though the original suggestion works, they've been removed in later
version and is no longer the best practices.

e.g., many sql realted operators have been removed and are now suggested
to use SQLExecuteQueryOperator instead

## Test Plan

The existing test fixtures have been updated
2025-04-25 12:44:28 -04:00
Andrew Gallant
049280a3bc red_knot_project: sort diagnostics from checking files
Previously, we could iterate over files in an unspecified order (via
`HashSet` iteration) and we could accumulate diagnostics from files in
an unspecified order (via parallelism).

Here, we change the status quo so that diagnostics collected from files
are sorted after checking is complete. For now, we sort by severity
(with higher severity diagnostics appearing first) and then by
diagnostic ID to give a stable ordering.

I'm not sure if this is the best ordering.
2025-04-25 12:38:31 -04:00
Carl Meyer
fa88989ef0 [red-knot] fix detecting a metaclass on a not-explicitly-specialized generic base (#17621)
## Summary

After https://github.com/astral-sh/ruff/pull/17620 (which this PR is
based on), I was looking at other call sites of `Type::into_class_type`,
and I began to feel that _all_ of them were currently buggy due to
silently skipping unspecialized generic class literal types (though in
some cases the bug hadn't shown up yet because we don't understand
legacy generic classes from typeshed), and in every case they would be
better off if an unspecialized generic class literal were implicitly
specialized with the default specialization (which is the usual Python
typing semantics for an unspecialized reference to a generic class),
instead of silently skipped.

So I changed the method to implicitly apply the default specialization,
and added a test that previously failed for detecting metaclasses on an
unspecialized generic base.

I also renamed the method to `to_class_type`, because I feel we have a
strong naming convention where `Type::into_foo` is always a trivial
`const fn` that simply returns `Some()` if the type is of variant `Foo`
and `None` otherwise. Even the existing method (with it handling both
`GenericAlias` and `ClassLiteral`, and distinguishing kinds of
`ClassLiteral`) was stretching this convention, and the new version
definitely breaks that envelope.

## Test Plan

Added a test that failed before this PR.
2025-04-25 06:55:54 -07:00
Carl Meyer
4c3f389598 [red-knot] fix inheritance-cycle detection for generic classes (#17620)
## Summary

The `ClassLiteralType::inheritance_cycle` method is intended to detect
inheritance cycles that would result in cyclic MROs, emit a diagnostic,
and skip actually trying to create the cyclic MRO, falling back to an
"error" MRO instead with just `Unknown` and `object`.

This method didn't work properly for generic classes. It used
`fully_static_explicit_bases`, which filter-maps `explicit_bases` over
`Type::into_class_type`, which returns `None` for an unspecialized
generic class literal. So in a case like `class C[T](C): ...`, because
the explicit base is an unspecialized generic, we just skipped it, and
failed to detect the class as cyclically defined.

Instead, iterate directly over all `explicit_bases`, and explicitly
handle both the specialized (`GenericAlias`) and unspecialized
(`ClassLiteral`) cases, so that we check all bases and correctly detect
cyclic inheritance.

## Test Plan

Added mdtests.
2025-04-25 06:55:00 -07:00
Brent Westbrook
6d3b1d13d6 [pylint] Detect global declarations in module scope (PLE0118) (#17411)
Summary
--

While going through the syntax errors in [this comment], I was surprised
to see the error `name 'x' is assigned to before global declaration`,
which corresponds to [load-before-global-declaration (PLE0118)] and has
also been reimplemented as a syntax error (#17135). However, it looks
like neither of the implementations consider `global` declarations in
the top-level module scope, which is a syntax error in CPython:

```python
# try.py
x = None
global x
```

```shell
> python -m compileall -f try.py
Compiling 'try.py'...
***   File "try.py", line 2
    global x
    ^^^^^^^^
SyntaxError: name 'x' is assigned to before global declaration
```

I'm not sure this is the best or most elegant solution, but it was a
quick fix that passed all of our tests.

Test Plan
--

New PLE0118 test case.

[this comment]:
https://github.com/astral-sh/ruff/issues/7633#issuecomment-1740424031
[load-before-global-declaration (PLE0118)]:
https://docs.astral.sh/ruff/rules/load-before-global-declaration/#load-before-global-declaration-ple0118
2025-04-25 08:37:16 -04:00
Max Mynter
3f84e75e20 Add Semantic Error Test for LateFutureImport (#17612)
Adresses a question in #17526.

## Summary
Adds a syntax error test for `__future__` import not at top of file. 

## Question: 
Is this a redundant with
8d2c79276d/crates/ruff_linter/resources/test/fixtures/pyflakes/F404_0.py (L1-L8)
and
8d2c79276d/crates/ruff_linter/resources/test/fixtures/pyflakes/F404_1.py (L1-L5)

which test pyflake `F404`?
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan
This is a test
<!-- How was it tested? -->
2025-04-25 08:32:57 -04:00
Carl Meyer
afc18ff1a1 [red-knot] change TypeVarInstance to be interned, not tracked (#17616)
## Summary

Tracked structs have some issues with fixpoint iteration in Salsa, and
there's not actually any need for this to be tracked, it should be
interned like most of our type structs.

The removed comment was probably never correct (in that we could have
disambiguated sufficiently), and is definitely not relevant now that
`TypeVarInstance` also holds its `Definition`.

## Test Plan

Existing tests.
2025-04-24 14:52:25 -07:00
Dhruv Manilawala
f1a539dac6 [red-knot] Special case @final, @override (#17608)
## Summary

This PR adds special-casing for `@final` and `@override` decorator for a
similar reason as https://github.com/astral-sh/ruff/pull/17591 to
support the invalid overload check.

Both `final` and `override` are identity functions which can be removed
once `TypeVar` support is added.
2025-04-25 03:15:23 +05:30
Carl Meyer
ef0343189c [red-knot] add TODO comment in specialization code (#17615)
## Summary

As promised, this just adds a TODO comment to document something we
discussed today that should probably be improved at some point, but
isn't a priority right now (since it's an issue that in practice would
only affect generic classes with both `__init__` and `__new__` methods,
where some typevar is bound to `Unknown` in one and to some other type
in another.)
2025-04-24 14:41:19 -07:00
Vasco Schiavo
4eecc40110 [semantic-syntax-errors] test for LoadBeforeGlobalDeclaration - ruff linter (#17592)
Hey @ntBre 

just one easy case to see if I understood the issue #17526 

Let me know if is this what you had in mind.
2025-04-24 16:14:33 -04:00
Abhijeet Prasad Bodas
cf59cee928 [syntax-errors] nonlocal declaration at module level (#17559)
## Summary

Part of #17412

Add a new compile-time syntax error for detecting `nonlocal`
declarations at a module level.

## Test Plan

- Added new inline tests for the syntax error
- Updated existing tests for `nonlocal` statement parsing to be inside a
function scope

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-04-24 16:11:46 -04:00
Wei Lee
538393d1f3 [airflow] Apply auto fix to cases where name has been changed in Airflow 3 (AIR311) (#17571)
## Summary

Apply auto fix to cases where the name has been changed in Airflow 3
(`AIR311`)

## Test Plan

The test features has been updated
2025-04-24 15:48:54 -04:00
Brent Westbrook
92ecfc908b [syntax-errors] Make async-comprehension-in-sync-comprehension more specific (#17460)
## Summary

While adding semantic error support to red-knot, I noticed duplicate
diagnostics for code like this:

```py
# error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)"
# error: [invalid-syntax] "`asynchronous comprehension` outside of an asynchronous function"
 [reveal_type(x) async for x in AsyncIterable()]
```

Beyond the duplication, the first error message doesn't make much sense
because this syntax is _not_ allowed on Python 3.11 either.

To fix this, this PR renames the
`async-comprehension-outside-async-function` semantic syntax error to
`async-comprehension-in-sync-comprehension` and fixes the rule to avoid
applying outside of sync comprehensions at all.

## Test Plan

New linter test demonstrating the false positive. The mdtests from my red-knot 
PR also reflect this change.
2025-04-24 15:45:54 -04:00
Dylan
f7b48510b5 Bump 0.11.7 (#17613) 2025-04-24 13:06:38 -05:00
Dhruv Manilawala
9937064761 [red-knot] Use iterative approach to collect overloads (#17607)
## Summary

This PR updates the `to_overloaded` method to use an iterative approach
instead of a recursive one.

Refer to
https://github.com/astral-sh/ruff/pull/17585#discussion_r2056804587 for
context.

The main benefit here is that it avoids calling the `to_overloaded`
function in a recursive manner which is a salsa query. So, this is a bit
hand wavy but we should also see less memory used because the cache will
only contain a single entry which should be the entire overload chain.
Previously, the recursive approach would mean that each of the function
involved in an overload chain would have a cache entry. This reduce in
memory shouldn't be too much and I haven't looked at the actual data for
it.

## Test Plan

Existing test cases should pass.
2025-04-24 22:23:50 +05:30
Andrew Gallant
8d2c79276d red_knot_python_semantic: avoid Rust's screaming snake case convention in mdtest 2025-04-24 11:43:01 -04:00
Andrew Gallant
0f47810768 red_knot_python_semantic: improve diagnostics for unsupported boolean conversions
This mostly only improves things for incorrect arguments and for an
incorrect return type. It doesn't do much to improve the case where
`__bool__` isn't callable and leaves the union/other cases untouched
completely.

I picked this one because, at first glance, this _looked_ like a lower
hanging fruit. The conceptual improvement here is pretty
straight-forward: add annotations for relevant data. But it took me a
bit to figure out how to connect all of the pieces.
2025-04-24 11:43:01 -04:00
Andrew Gallant
eb1d2518c1 red_knot_python_semantic: add "return type span" helper method
This is very similar to querying for the span of a parameter
in a function definition, but instead we look for the span of
a return type.
2025-04-24 11:43:01 -04:00
Andrew Gallant
a45a0a92bd red_knot_python_semantic: move parameter span helper method
I wanted to use this method in other places, so I moved it
to what appears to be a God-type. I also made it slightly
more versatile: callers can ask for the entire parameter list
by omitting a specific parameter index.
2025-04-24 11:43:01 -04:00
Andrew Gallant
43bd043755 ruff_db: add a From impl for FileRange to Span
These types are almost equivalent. The only difference
is that a `Span`'s range is optional.
2025-04-24 11:43:01 -04:00
Andrew Gallant
9a54ee3a1c red_knot_python_semantic: add snapshot tests for unsupported boolean conversions
This just captures the status quo before we try to improve them.
2025-04-24 11:43:01 -04:00
Carl Meyer
25c3be51d2 [red-knot] simplify != narrowing (#17610)
## Summary

Follow-up from review comment in
https://github.com/astral-sh/ruff/pull/17567#discussion_r2058649527

## Test Plan

Existing tests.
2025-04-24 15:11:45 +00:00
Matthew Mckee
e71f3ed2c5 [red-knot] Update == and != narrowing (#17567)
## Summary

Historically we have avoided narrowing on `==` tests because in many
cases it's unsound, since subclasses of a type could compare equal to
who-knows-what. But there are a lot of types (literals and unions of
them, as well as some known instances like `None` -- single-valued
types) whose `__eq__` behavior we know, and which we can safely narrow
away based on equality comparisons.

This PR implements equality narrowing in the cases where it is sound.
The most elegant way to do this (and the way that is most in-line with
our approach up until now) would be to introduce new Type variants
`NeverEqualTo[...]` and `AlwaysEqualTo[...]`, and then implement all
type relations for those variants, narrow by intersection, and let union
and intersection simplification sort it all out. This is analogous to
our existing handling for `AlwaysFalse` and `AlwaysTrue`.

But I'm reluctant to add new `Type` variants for this, mostly because
they could end up un-simplified in some types and make types even more
complex. So let's try this approach, where we handle more of the
narrowing logic as a special case.

## Test Plan

Updated and added tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-24 07:56:39 -07:00
Carl Meyer
ac6219ec38 [red-knot] fix collapsing literal and its negation to object (#17605)
## Summary

Another follow-up to the unions-of-large-literals optimization. Restore
the behavior that e.g. `Literal[""] | ~Literal[""]` collapses to
`object`.

## Test Plan

Added mdtests.
2025-04-24 13:55:05 +00:00
Alex Waygood
e93fa7062c [red-knot] Add more tests for protocols (#17603) 2025-04-24 13:11:31 +01:00
Alex Waygood
21fd28d713 [red-knot] Ban direct instantiations of Protocol classes (#17597) 2025-04-24 09:31:35 +00:00
Max Mynter
a01f25107a [pyupgrade] Preserve parenthesis when fixing native literals containing newlines (UP018) (#17220) 2025-04-24 08:48:02 +02:00
camper42
48a85c4ed4 [airflow] fix typos (AIR302, AIR312) (#17574) 2025-04-24 08:06:32 +02:00
Dhruv Manilawala
1796ca97d5 [red-knot] Special case @abstractmethod for function type (#17591)
## Summary

This is required because otherwise the inferred type is not going to be
`Type::FunctionLiteral` but a todo type because we don't recognize
`TypeVar` yet:

```py
_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])

def abstractmethod(funcobj: _FuncT) -> _FuncT: ...
```

This is mainly required to raise diagnostic when only some (and not all)
`@overload`-ed functions are decorated with `@abstractmethod`.
2025-04-24 03:54:52 +05:30
Alex Waygood
e897f37911 [red-knot] Emit diagnostics for isinstance() and issubclass() calls where a non-runtime-checkable protocol is the second argument (#17561) 2025-04-23 21:40:23 +00:00
Alex Waygood
00e73dc331 [red-knot] Infer the members of a protocol class (#17556) 2025-04-23 21:36:12 +00:00
Dhruv Manilawala
7b6222700b [red-knot] Add FunctionType::to_overloaded (#17585)
## Summary

This PR adds a new method `FunctionType::to_overloaded` which converts a
`FunctionType` into an `OverloadedFunction` which contains all the
`@overload`-ed `FunctionType` and the implementation `FunctionType` if
it exists.

There's a big caveat here (it's the way overloads work) which is that
this method can only "see" all the overloads that comes _before_ itself.
Consider the following example:

```py
from typing import overload

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

Here, when the `to_overloaded` method is invoked on the
1. first `foo` definition, it would only contain a single overload which
is itself and no implementation.
2. second `foo` definition, it would contain both overloads and still no
implementation
3. third `foo` definition, it would contain both overloads and the
implementation which is itself

### Usages

This method will be used in the logic for checking invalid overload
usages. It can also be used for #17541.

## Test Plan

Make sure that existing tests pass.
2025-04-24 02:57:05 +05:30
Brent Westbrook
bfc1650198 [red-knot] Add mdtests for global statement (#17563)
## Summary

This is a first step toward `global` support in red-knot (#15385). I
went through all the matches for `global` in the `mypy/test-data`
directory, but I didn't find anything too interesting that wasn't
already covered by @carljm's suggestions on Discord. I still pulled in a
couple of cases for a little extra variety. I also included a section
from the
[PLE0118](https://docs.astral.sh/ruff/rules/load-before-global-declaration/)
tests in ruff that will become syntax errors once #17463 is merged and
we handle `global` statements.

I don't think I figured out how to use `@Todo` properly, so please let
me know if I need to fix that. I hope this is a good start to the test
suite otherwise.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-23 17:18:42 -04:00
Brent Westbrook
d5410ef9fe [syntax-errors] Make duplicate parameter names a semantic error (#17131)
Status
--

This is a pretty minor change, but it was breaking a red-knot mdtest
until #17463 landed. Now this should close #11934 as the last syntax
error being tracked there!

Summary
--

Moves `Parser::validate_parameters` to
`SemanticSyntaxChecker::duplicate_parameter_name`.

Test Plan
--

Existing tests, with `## Errors` replaced with `## Semantic Syntax
Errors`.
2025-04-23 15:45:51 -04:00
Douglas Creager
9db63fc58c [red-knot] Handle generic constructors of generic classes (#17552)
We now handle generic constructor methods on generic classes correctly:

```py
class C[T]:
    def __init__[S](self, t: T, s: S): ...

x = C(1, "str")
```

Here, constructing `C` requires us to infer a specialization for the
generic contexts of `C` and `__init__` at the same time.

At first I thought I would need to track the full stack of nested
generic contexts here (since the `[S]` context is nested within the
`[T]` context). But I think this is the only way that we might need to
specialize more than one generic context at once — in all other cases, a
containing generic context must be specialized before we get to a nested
one, and so we can just special-case this.

While we're here, we also construct the generic context for a generic
function lazily, when its signature is accessed, instead of eagerly when
inferring the function body.
2025-04-23 15:06:18 -04:00
David Peter
61e73481fe [red-knot] Assignability of class instances to Callable (#17590)
## Summary

Model assignability of class instances with a `__call__` method to
`Callable` types. This should solve some false positives related to
`functools.partial` (yes, 1098 fewer diagnostics!).

Reference:
https://github.com/astral-sh/ruff/issues/17343#issuecomment-2824618483

## Test Plan

New Markdown tests.
2025-04-23 20:34:13 +02:00
David Peter
e170fe493d [red-knot] Trust all symbols in stub files (#17588)
## Summary

*Generally* trust undeclared symbols in stubs, not just at the module
level.

Follow-up on the discussion
[here](https://github.com/astral-sh/ruff/pull/17577#discussion_r2055945909).

## Test Plan

New Markdown test.
2025-04-23 20:07:29 +02:00
David Peter
e91e2f49db [red-knot] Trust module-level undeclared symbols in stubs (#17577)
## Summary

Many symbols in typeshed are defined without being declared. For
example:
```pyi
# builtins:
IOError = OSError

# types
LambdaType = FunctionType
NotImplementedType = _NotImplementedType

# typing
Text = str

# random
uniform = _inst.uniform

# optparse
make_option = Option

# all over the place:
_T = TypeVar("_T")
```

Here, we introduce a change that skips widening the public type of these
symbols (by unioning with `Unknown`).

fixes #17032

## Ecosystem analysis

This is difficult to analyze in detail, but I went over most changes and
it looks very favorable to me overall. The diff on the overall numbers
is:
```
errors: 1287 -> 859 (reduction by 428)
warnings: 45 -> 59 (increase by 14)
```

### Removed false positives

`invalid-base` examples:

```diff
- error[lint:invalid-base] /tmp/mypy_primer/projects/pip/src/pip/_vendor/rich/console.py:548:27: Invalid class base with type `Unknown | Literal[_local]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
- error[lint:invalid-base] /tmp/mypy_primer/projects/tornado/tornado/iostream.py:84:25: Invalid class base with type `Unknown | Literal[OSError]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
- error[lint:invalid-base] /tmp/mypy_primer/projects/mitmproxy/test/conftest.py:35:40: Invalid class base with type `Unknown | Literal[_UnixDefaultEventLoopPolicy]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
```

`invalid-exception-caught` examples:

```diff
- error[lint:invalid-exception-caught] /tmp/mypy_primer/projects/cloud-init/cloudinit/cmd/status.py:334:16: Cannot catch object of type `Literal[ProcessExecutionError]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)
- error[lint:invalid-exception-caught] /tmp/mypy_primer/projects/jinja/src/jinja2/loaders.py:537:16: Cannot catch object of type `Literal[TemplateNotFound]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)
```

`unresolved-reference` examples


7a0265d36e/cloudinit/handlers/jinja_template.py (L120-L123)
(we now understand the `isinstance` narrowing)

```diff
- error[lint:unresolved-attribute] /tmp/mypy_primer/projects/cloud-init/cloudinit/handlers/jinja_template.py:123:16: Type `Exception` has no attribute `errno`
```

`unknown-argument` examples


https://github.com/hauntsaninja/boostedblob/blob/master/boostedblob/request.py#L53

```diff
- error[lint:unknown-argument] /tmp/mypy_primer/projects/boostedblob/boostedblob/request.py:53:17: Argument `connect` does not match any known parameter of bound method `__init__`
```

`unknown-argument`

There are a lot of `__init__`-related changes because we now understand
[`@attr.s`](3d42a6978a/src/attr/__init__.pyi (L387))
as a `@dataclass_transform` annotated symbol. For example:

```diff
- error[lint:unknown-argument] /tmp/mypy_primer/projects/attrs/tests/test_hooks.py:72:18: Argument `x` does not match any known parameter of bound method `__init__`
```

### New false positives

This can happen if a symbol that previously was inferred as `X |
Unknown` was assigned-to, but we don't yet understand the assignability
to `X`:


https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/exceptions/handler.py#L90

```diff
+ error[lint:invalid-assignment] /tmp/mypy_primer/projects/strawberry/strawberry/exceptions/handler.py:90:9: Object of type `def strawberry_threading_exception_handler(args: tuple[type[BaseException], BaseException | None, TracebackType | None, Thread | None]) -> None` is not assignable to attribute `excepthook` of type `(_ExceptHookArgs, /) -> Any`
```

### New true positives


6bbb5519fe/tests/tracer/test_span.py (L714)

```diff
+ error[lint:invalid-argument-type] /tmp/mypy_primer/projects/dd-trace-py/tests/tracer/test_span.py:714:33: Argument to this function is incorrect: Expected `str`, found `Literal[b"\xf0\x9f\xa4\x94"]`
```

### Changed diagnostics

A lot of changed diagnostics because we now show `@Todo(Support for
`typing.TypeVar` instances in type expressions)` instead of `Unknown`
for all kinds of symbols that used a `_T = TypeVar("_T")` as a type. One
prominent example is the `list.__getitem__` method:

`builtins.pyi`:
```pyi
_T = TypeVar("_T")  # previously `TypeVar | Unknown`, now just `TypeVar`

# …

class list(MutableSequence[_T]):
    # …
    @overload
    def __getitem__(self, i: SupportsIndex, /) -> _T: ...
    # …
```

which causes this change in diagnostics:
```py
xs = [1, 2]
reveal_type(xs[0])  # previously `Unknown`, now `@Todo(Support for `typing.TypeVar` instances in type expressions)`
```

## Test Plan

Updated Markdown tests
2025-04-23 19:31:14 +02:00
Wei Lee
b537552927 [airflow] Apply auto fixes to cases where the names have changed in Airflow 3 (AIR301) (#17355)
## Summary

Apply auto fixes to cases where the names have changed in Airflow 3

## Test Plan

Add `AIR301_names_fix.py` and `AIR301_provider_names_fix.py` test fixtures
2025-04-23 12:43:41 -04:00
Navdeep K
5a719f2d60 [pycodestyle] Auto-fix redundant boolean comparison (E712) (#17090)
This pull request fixes https://github.com/astral-sh/ruff/issues/17014

changes this
```python
from __future__ import annotations

flag1 = True
flag2 = True

if flag1 == True or flag2 == True:
    pass

if flag1 == False and flag2 == False:
    pass

flag3 = True
if flag1 == flag3 and (flag2 == False or flag3 == True):  # Should become: if flag1==flag3 and (not flag2 or flag3)
    pass

if flag1 == True and (flag2 == False or not flag3 == True):  # Should become: if flag1 and (not flag2 or not flag3)
    pass

if flag1 != True and (flag2 != False or not flag3 == True):  # Should become: if not flag1 and (flag2 or not flag3)
    pass


flag = True
while flag == True:  # Should become: while flag
    flag = False

flag = True
x = 5
if flag == True and x > 0:  # Should become: if flag and x > 0
    print("ok")

flag = True
result = "yes" if flag == True else "no"  # Should become: result = "yes" if flag else "no"

x = flag == True < 5

x = (flag == True) == False < 5
```

to this 
```python
from __future__ import annotations

flag1 = True
flag2 = True

if flag1 or flag2:
    pass

if not flag1 and not flag2:
    pass

flag3 = True
if flag1 == flag3 and (not flag2 or flag3):  # Should become: if flag1 == flag3 and (not flag2 or flag3)
    pass

if flag1 and (not flag2 or not flag3):  # Should become: if flag1 and (not flag2 or not flag3)
    pass

if not flag1 and (flag2 or not flag3):  # Should become: if not flag1 and (flag2 or not flag3)
    pass


flag = True
while flag:  # Should become: while flag
    flag = False

flag = True
x = 5
if flag and x > 0:  # Should become: if flag and x > 0
    print("ok")

flag = True
result = "yes" if flag else "no"  # Should become: result = "yes" if flag else "no"

x = flag is True < 5

x = (flag) is False < 5
```

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-04-23 11:49:20 -04:00
Brent Westbrook
e7f38fe74b [red-knot] Detect semantic syntax errors (#17463)
Summary
--

This PR extends semantic syntax error detection to red-knot. The main
changes here are:

1. Adding `SemanticSyntaxChecker` and `Vec<SemanticSyntaxError>` fields
to the `SemanticIndexBuilder`
2. Calling `SemanticSyntaxChecker::visit_stmt` and `visit_expr` in the
`SemanticIndexBuilder`'s `visit_stmt` and `visit_expr` methods
3. Implementing `SemanticSyntaxContext` for `SemanticIndexBuilder`
4. Adding new mdtests to test the context implementation and show
diagnostics

(3) is definitely the trickiest and required (I think) a minor addition
to the `SemanticIndexBuilder`. I tried to look around for existing code
performing the necessary checks, but I definitely could have missed
something or misused the existing code even when I found it.

There's still one TODO around `global` statement handling. I don't think
there's an existing way to look this up, but I'm happy to work on that
here or in a separate PR. This currently only affects detection of one
error (`LoadBeforeGlobalDeclaration` or
[PLE0118](https://docs.astral.sh/ruff/rules/load-before-global-declaration/)
in ruff), so it's not too big of a problem even if we leave the TODO.

Test Plan
--

New mdtests, as well as new errors for existing mdtests

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-23 09:52:58 -04:00
Micha Reiser
624f5c6c22 Fix stale diagnostics in Ruff playground (#17583) 2025-04-23 15:47:54 +02:00
Micha Reiser
8abf93f5fb [red-knot] Early return from project.is_file_open for vendored files (#17580) 2025-04-23 15:32:41 +02:00
Micha Reiser
5407249467 [red-knot] Make BoundMethodType a salsa interned (#17581) 2025-04-23 15:11:20 +02:00
Alex Waygood
0a1f9d090e [red-knot] Emit a diagnostic if a non-protocol is passed to get_protocol_members (#17551) 2025-04-23 10:13:20 +00:00
Alex Waygood
f9c7908bb7 [red-knot] Add more tests for protocol members (#17550) 2025-04-23 11:03:52 +01:00
David Peter
99fa850e53 [red-knot] Assignability for subclasses of Any and Unknown (#17557)
## Summary

Allow (instances of) subclasses of `Any` and `Unknown` to be assignable
to (instances of) other classes, unless they are final. This allows us
to get rid of ~1000 false positives, mostly when mock-objects like
`unittest.mock.MagicMock` are assigned to various targets.

## Test Plan

Adapted and new Markdown tests.
2025-04-23 11:37:30 +02:00
David Peter
a241321735 [red-knot] mypy_primer: add strawberry, print compilation errors to stderr (#17578)
## Summary

mypy_primer changes included here:
ebaa9fd27b..4c22d192a4

- Add strawberry as a `good.txt` project (was previously included in our
fork)
- Print Red Knot compilation errors to stderr (thanks @MichaReiser)
2025-04-23 10:57:11 +02:00
David Peter
b1b8ca3bcd [red-knot] GenericAlias instances as a base class (#17575)
## Summary

We currently emit a diagnostic for code like the following:
```py
from typing import Any

# error: Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)
class C(tuple[Any, ...]): ...
```

The changeset here silences this diagnostic by recognizing instances of
`GenericAlias` in `ClassBase::try_from_type`, and inferring a `@Todo`
type for them. This is a change in preparation for #17557, because `C`
previously had `Unknown` in its MRO …
```py
reveal_type(C.__mro__)  # tuple[Literal[C], Unknown, Literal[object]]
```
… which would cause us to think that `C` is assignable to everything.

The changeset also removes some false positive `invalid-base`
diagnostics across the ecosystem.

## Test Plan

Updated Markdown tests.
2025-04-23 10:39:10 +02:00
Shaygan Hooshyari
3fae176345 Remove redundant type_to_visitor_function entries (#17564) 2025-04-23 09:27:00 +02:00
David Salvisberg
f36262d970 Fixes how the checker visits typing.cast/typing.NewType arguments (#17538) 2025-04-23 09:26:00 +02:00
Matthew Mckee
e45f23b0ec [red-knot] Class literal __new__ function callable subtyping (#17533)
## Summary

From
https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable

this covers step 2 and partially step 3 (always respecting the
`__new__`)

## Test Plan

Update is_subtype_of.md

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-22 22:40:33 -07:00
Matthew Mckee
aa46047649 [red-knot] Surround intersections with () in potentially ambiguous contexts (#17568)
## Summary

Add parentheses to multi-element intersections, when displayed in a
context that's otherwise potentially ambiguous.

## Test Plan

Update mdtest files

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-23 04:18:20 +00:00
Brent Westbrook
f9da115fdc [minor] Delete outdated TODO comment (#17565)
Summary
--

Delete a TODO I left that was handled in the last minor release
(#16125).

Test Plan
--

N/a
2025-04-22 20:23:08 +00:00
Carl Meyer
3872d57463 [red-knot] add regression test for fixed cycle panic (#17535)
Add a regression test for the cycle documented in
https://github.com/astral-sh/ruff/issues/14767, which no longer panics
(or even causes a cycle at all.)

Fixes https://github.com/astral-sh/ruff/issues/14767
2025-04-22 09:20:53 -07:00
Carl Meyer
27ada26ddb [red-knot] fix unions of literals, again (#17534)
## Summary

#17451 was incomplete. `AlwaysFalsy` and `AlwaysTruthy` are not the only
two types that are super-types of some literals (of a given kind) and
not others. That set also includes intersections containing
`AlwaysTruthy` or `AlwaysFalsy`, and intersections containing literal
types of the same kind. Cover these cases as well.

Fixes #17478.

## Test Plan

Added mdtests.

`QUICKCHECK_TESTS=1000000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable` failed on both
`all_fully_static_type_pairs_are_subtypes_of_their_union` and
`all_type_pairs_are_assignable_to_their_union` prior to this PR, passes
after it.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-22 16:12:52 +00:00
Andrew Gallant
810478f68b red_knot_python_semantic: remove last vestige of old diagnostics! 2025-04-22 12:08:03 -04:00
Andrew Gallant
17f799424a red_knot_python_semantic: migrate types to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
c12640fea8 red_knot_python_semantic: migrate types/diagnostic to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
3796b13ea2 red_knot_python_semantic: migrate types/call/bind to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
ad5a659f29 red_knot_python_semantic: migrate types/string_annotation to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
27a377f077 red_knot_python_semantic: migrate types/infer to new diagnostic model
I gave up trying to do this one lint at a time and just (mostly)
mechanically translated this entire file in one go.

Generally the messages stay the same (with most moving from an
annotation message to the diagnostic's main message). I added a couple
of `info` sub-diagnostics where it seemed to be the obvious intent.
2025-04-22 12:08:03 -04:00
Andrew Gallant
b8b624d890 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for inference
This finishes the migration for the `INVALID_ASSIGNMENT` lint.

Notice how I'm steadily losing steam in terms of actually improving the
diagnostics. This change is more mechanical, because taking the time to
revamp every diagnostic is a ton of effort. Probably future migrations
will be similar unless there are easy pickings.
2025-04-22 12:08:03 -04:00
Andrew Gallant
6dc2d29966 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for shadowing
We mostly keep things the same here, but the message has been moved from
the annotation to the diagnostic's top-line message. I think this is
perhaps a little worse, but some bigger improvements could be made here.
Indeed, we could perhaps even add a "fix" here.
2025-04-22 12:08:03 -04:00
Andrew Gallant
890ba725d9 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for unpacking
This moves all INVALID_ASSIGNMENT lints related to unpacking over to the new
diagnostic model.

While we're here, we improve the diagnostic a bit by adding a secondary
annotation covering where the value is. We also split apart the original
singular message into one message for the diagnostic and the "expected
versus got" into annotation messages.
2025-04-22 12:08:03 -04:00
Andrew Gallant
298f43f34e red_knot_python_semantic: add invalid assignment diagnostic snapshot
This tests the diagnostic rendering of a case that wasn't previously
covered by snapshots: when unpacking fails because there are too few
values, but where the left hand side can tolerate "N or more." In the
code, this is a distinct diagnostic, so we capture it here.

(Sorry about the diff here, but it made sense to rename the other
sections and that changes the name of the snapshot file.)
2025-04-22 12:08:03 -04:00
Andrew Gallant
3b300559ab red_knot_python_semantic: remove #[must_use] on diagnostic guard constructor
I believe this was an artifact of an older iteration of the diagnostic
reporting API. But this is strictly not necessary now, and indeed, might
even be annoying. It is okay, but perhaps looks a little odd, to do
`builder.into_diagnostic("...")` if you don't want to add anything else
to the diagnostic.
2025-04-22 12:08:03 -04:00
Andrew Gallant
14f71ceb83 red_knot_python_semantic: add helper method for creating a secondary annotation
I suspect this will be used pretty frequently (I wanted it
immediately). And more practically, this avoids needing to
import `Annotation` to create it.
2025-04-22 12:08:03 -04:00
David Peter
4775719abf [red-knot] mypy_primer: larger depot runner (#17547)
## Summary

A switch from 16 to 32 cores reduces the `mypy_primer` CI time from
3.5-4 min to 2.5-3 min. There's also a 64-core runner, but the 4 min ->
3 min change when doubling the cores once does suggest that it doesn't
parallelize *this* well.
2025-04-22 17:36:13 +02:00
Alex Waygood
6bdffc3cbf [red-knot] Consider two instance types disjoint if the underlying classes have disjoint metaclasses (#17545) 2025-04-22 15:14:10 +01:00
Aria Desires
775815ef22 Update cargo-dist and apply config improvements (#17453) 2025-04-22 10:05:15 -04:00
Carl Meyer
0299a52fb1 [red-knot] Add list of failing/slow ecosystem projects (#17474)
## Summary

I ran red-knot on every project in mypy-primer. I moved every project
where red-knot ran to completion (fast enough, and mypy-primer could
handle its output) into `good.txt`, so it will run in our CI.

The remaining projects I left listed in `bad.txt`, with a comment
summarizing the failure mode (a few don't fail, they are just slow -- on
a debug build, at least -- or output too many diagnostics for
mypy-primer to handle.)

We will now run CI on 109 projects; 34 are left in `bad.txt`.

## Test Plan

CI on this PR!

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-04-22 14:15:36 +02:00
David Peter
83d5ad8983 [red-knot] mypy_primer: extend ecosystem checks (#17544)
## Summary

Takes the `good.txt` changes from #17474, and removes the following
projects:
- arrow (not part of mypy_primer upstream)
- freqtrade, hydpy, ibis, pandera, xarray (saw panics locally, all
related to try_metaclass cycles)

Increases the mypy_primer CI run time to ~4 min.

## Test Plan

Three successful CI runs.
2025-04-22 13:39:42 +02:00
Alex Waygood
ae6fde152c [red-knot] Move InstanceType to its own submodule (#17525) 2025-04-22 11:34:46 +00:00
David Peter
d2b20f7367 [red-knot] mypy_primer: capture backtraces (#17543)
## Summary

`mypy_primer` is not deterministic (we pin `mypy_primer` itself, but
projects change over time and we just pull in the latest version). We've
also seen occasional panics being caught in `mypy_primer` runs, so this
is trying to make these CI failures more helpful.
2025-04-22 12:05:57 +02:00
David Peter
38a3b056e3 [red-knot] mypy_primer: Use upstream repo (#17500)
## Summary

Switch to the official version of
[`mypy_primer`](https://github.com/hauntsaninja/mypy_primer), now that
Red Knot support has been upstreamed (see
https://github.com/hauntsaninja/mypy_primer/pull/138,
https://github.com/hauntsaninja/mypy_primer/pull/135,
https://github.com/hauntsaninja/mypy_primer/pull/151,
https://github.com/hauntsaninja/mypy_primer/pull/155).

## Test Plan

Locally and in CI
2025-04-22 11:55:16 +02:00
David Peter
37a0836bd2 [red-knot] typing.dataclass_transform (#17445)
## Summary

* Add initial support for `typing.dataclass_transform`
* Support decorating a function decorator with `@dataclass_transform(…)`
(used by `attrs`, `strawberry`)
* Support decorating a metaclass with `@dataclass_transform(…)` (used by
`pydantic`, but doesn't work yet, because we don't seem to model
`__new__` calls correctly?)
* *No* support yet for decorating base classes with
`@dataclass_transform(…)`. I haven't figured out how this even supposed
to work. And haven't seen it being used.
* Add `strawberry` as an ecosystem project, as it makes heavy use of
`@dataclass_transform`

## Test Plan

New Markdown tests
2025-04-22 10:33:02 +02:00
renovate[bot]
f83295fe51 Update dependency react-resizable-panels to v2.1.8 (#17513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:30:07 +02:00
renovate[bot]
c4581788b2 Update dependency smol-toml to v1.3.3 (#17505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:19:55 +02:00
renovate[bot]
2894aaa943 Update dependency uuid to v11.1.0 (#17517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:18:54 +02:00
renovate[bot]
ed4866a00b Update actions/setup-node action to v4.4.0 (#17514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:18:13 +02:00
Matthew Mckee
9b5fe51b32 [red-knot] Fix variable name (#17532) 2025-04-21 17:20:04 -07:00
Matthew Mckee
53ffe7143f [red-knot] Add basic subtyping between class literal and callable (#17469)
## Summary

This covers step 1 from
https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable

Part of #17343

## Test Plan

Update is_subtype_of.md and is_assignable_to.md

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-21 22:29:36 +00:00
Hans
21561000b1 [pyupgrade] Add fix safety section to docs (UP030) (#17443)
## Summary

add fix safety section to format_literals, for #15584
2025-04-21 14:14:58 -04:00
w0nder1ng
9c0772d8f0 [perflint] Allow list function calls to be replaced with a comprehension (PERF401) (#17519)
This is an implementation of the discussion from #16719. 

This change will allow list function calls to be replaced with
comprehensions:

```python
result = list()
for i in range(3):
    result.append(i + 1)
# becomes
result = [i + 1 for i in range(3)]
```

I added a new test to `PERF401.py` to verify that this fix will now work
for `list()`.
2025-04-21 13:29:24 -04:00
411 changed files with 15907 additions and 8440 deletions

6
.gitattributes vendored
View File

@@ -12,6 +12,12 @@ crates/ruff_python_parser/resources/invalid/re_lexing/line_continuation_windows_
crates/ruff_python_parser/resources/invalid/re_lex_logical_token_windows_eol.py text eol=crlf
crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac_eol.py text eol=cr
crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py text eol=cr
crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py text eol=lf
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py text eol=cr
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py text eol=lf
crates/ruff_python_parser/resources/inline linguist-generated=true
ruff.schema.json -diff linguist-generated=true text=auto eol=lf

View File

@@ -6,5 +6,6 @@ self-hosted-runner:
labels:
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- depot-ubuntu-22.04-32
- github-windows-2025-x86_64-8
- github-windows-2025-x86_64-16

View File

@@ -79,7 +79,7 @@ jobs:
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Build and push by digest
id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
with:
context: .
platforms: ${{ matrix.platform }}
@@ -231,7 +231,7 @@ jobs:
${{ env.TAG_PATTERNS }}
- name: Build and push
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
with:
context: .
platforms: linux/amd64,linux/arm64

View File

@@ -239,11 +239,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
with:
tool: cargo-insta
- name: Red-knot mdtests (GitHub annotations)
@@ -293,11 +293,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # 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@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
with:
tool: cargo-nextest
- name: "Run tests"
@@ -346,7 +346,7 @@ jobs:
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: "npm"
@@ -403,11 +403,11 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
with:
tool: cargo-insta
- name: "Run tests"
@@ -821,7 +821,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"
@@ -857,7 +857,7 @@ jobs:
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
uses: taiki-e/install-action@ab3728c7ba6948b9b429627f4d55a68842b27f18 # v2
with:
tool: cargo-codspeed

View File

@@ -21,11 +21,12 @@ env:
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
jobs:
mypy_primer:
name: Run mypy_primer
runs-on: depot-ubuntu-22.04-16
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -40,13 +41,10 @@ jobs:
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5"
- name: Run mypy_primer
shell: bash
run: |
@@ -66,7 +64,9 @@ jobs:
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 \
uvx \
--from="git+https://github.com/hauntsaninja/mypy_primer@b83b9eade0b7ed2f4b9b129b163acac1ecb48f71" \
mypy_primer \
--repo ruff \
--type-checker knot \
--old base_commit \

View File

@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"

View File

@@ -45,7 +45,7 @@ jobs:
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -40,6 +40,7 @@ permissions:
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
workflow_dispatch:
inputs:
tag:
@@ -60,7 +61,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
@@ -68,9 +69,9 @@ jobs:
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4-prerelease.1/cargo-dist-installer.sh | sh"
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
@@ -86,7 +87,7 @@ jobs:
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
@@ -123,19 +124,19 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: artifacts-*
path: target/distrib/
@@ -153,7 +154,7 @@ jobs:
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: artifacts-build-global
path: |
@@ -174,19 +175,19 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: artifacts-*
path: target/distrib/
@@ -200,7 +201,7 @@ jobs:
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
@@ -250,13 +251,13 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: artifacts-*
path: artifacts

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.6
rev: v0.11.7
hooks:
- id: ruff-format
- id: ruff

View File

@@ -1,5 +1,30 @@
# Changelog
## 0.11.7
### Preview features
- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR301`) ([#17355](https://github.com/astral-sh/ruff/pull/17355))
- \[`perflint`\] Implement fix for `manual-dict-comprehension` (`PERF403`) ([#16719](https://github.com/astral-sh/ruff/pull/16719))
- [syntax-errors] Make duplicate parameter names a semantic error ([#17131](https://github.com/astral-sh/ruff/pull/17131))
### Bug fixes
- \[`airflow`\] Fix typos in provider package names (`AIR302`, `AIR312`) ([#17574](https://github.com/astral-sh/ruff/pull/17574))
- \[`flake8-type-checking`\] Visit keyword arguments in checks involving `typing.cast`/`typing.NewType` arguments ([#17538](https://github.com/astral-sh/ruff/pull/17538))
- \[`pyupgrade`\] Preserve parenthesis when fixing native literals containing newlines (`UP018`) ([#17220](https://github.com/astral-sh/ruff/pull/17220))
- \[`refurb`\] Mark the `FURB161` fix unsafe except for integers and booleans ([#17240](https://github.com/astral-sh/ruff/pull/17240))
### Rule changes
- \[`perflint`\] Allow list function calls to be replaced with a comprehension (`PERF401`) ([#17519](https://github.com/astral-sh/ruff/pull/17519))
- \[`pycodestyle`\] Auto-fix redundant boolean comparison (`E712`) ([#17090](https://github.com/astral-sh/ruff/pull/17090))
- \[`pylint`\] make fix unsafe if delete comments (`PLR1730`) ([#17459](https://github.com/astral-sh/ruff/pull/17459))
### Documentation
- Add fix safety sections to docs for several rules ([#17410](https://github.com/astral-sh/ruff/pull/17410),[#17440](https://github.com/astral-sh/ruff/pull/17440),[#17441](https://github.com/astral-sh/ruff/pull/17441),[#17443](https://github.com/astral-sh/ruff/pull/17443),[#17444](https://github.com/astral-sh/ruff/pull/17444))
## 0.11.6
### Preview features

124
Cargo.lock generated
View File

@@ -394,7 +394,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -490,20 +490,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "compact_str"
version = "0.9.0"
@@ -724,7 +710,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -735,7 +721,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -805,7 +791,7 @@ dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -837,7 +823,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -1323,7 +1309,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -1488,7 +1474,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -1553,9 +1539,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.9"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ec30f7142be6fe14e1b021f50b85db8df2d4324ea6e91ec3e5dcde092021d0"
checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
@@ -1563,18 +1549,18 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
name = "jiff-static"
version = "0.2.9"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "526b834d727fd59d37b076b0c3236d9adde1b1729a4361e20b2026f738cc1dbe"
checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -1671,7 +1657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa96ed35d0dccc67cf7ba49350cb86de3dcb1d072a7ab28f99117f19d874953"
dependencies = [
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -2180,7 +2166,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -2249,7 +2235,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -2572,7 +2558,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.0",
"camino",
"compact_str 0.9.0",
"compact_str",
"countme",
"dir-test",
"drop_bomb",
@@ -2772,7 +2758,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.6"
version = "0.11.7"
dependencies = [
"anyhow",
"argfile",
@@ -3007,7 +2993,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.6"
version = "0.11.7"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3074,7 +3060,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3101,7 +3087,7 @@ version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.9.0",
"compact_str 0.9.0",
"compact_str",
"is-macro",
"itertools 0.14.0",
"memchr",
@@ -3199,7 +3185,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.0",
"bstr",
"compact_str 0.9.0",
"compact_str",
"insta",
"memchr",
"ruff_annotate_snippets",
@@ -3333,7 +3319,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.6"
version = "0.11.7"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3457,11 +3443,11 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
version = "0.20.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1"
dependencies = [
"boxcar",
"compact_str 0.8.1",
"compact_str",
"crossbeam-queue",
"dashmap 6.1.0",
"hashbrown 0.15.2",
@@ -3480,18 +3466,18 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
version = "0.20.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1"
[[package]]
name = "salsa-macros"
version = "0.19.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
version = "0.20.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=c75b0161aba55965ab6ad8cc9aaee7dc177967f1#c75b0161aba55965ab6ad8cc9aaee7dc177967f1"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
"synstructure",
]
@@ -3525,7 +3511,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3574,7 +3560,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3585,7 +3571,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3608,7 +3594,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3649,7 +3635,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3780,7 +3766,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3796,9 +3782,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.100"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
@@ -3813,7 +3799,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3884,7 +3870,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3895,7 +3881,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
"test-case-core",
]
@@ -3931,7 +3917,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -3942,7 +3928,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -4073,7 +4059,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -4340,7 +4326,7 @@ checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -4462,7 +4448,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
"wasm-bindgen-shared",
]
@@ -4497,7 +4483,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4532,7 +4518,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -4647,7 +4633,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -4658,7 +4644,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -4896,7 +4882,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
"synstructure",
]
@@ -4917,7 +4903,7 @@ checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]
@@ -4937,7 +4923,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
"synstructure",
]
@@ -4960,7 +4946,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"syn 2.0.101",
]
[[package]]

View File

@@ -124,7 +124,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c75b0161aba55965ab6ad8cc9aaee7dc177967f1" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -272,7 +272,9 @@ inherits = "release"
# Config for 'dist'
[workspace.metadata.dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.28.4-prerelease.1"
cargo-dist-version = "0.28.4"
# Make distability of apps opt-in instead of opt-out
dist = false
# CI backends to support
ci = "github"
# The installers to generate for each app
@@ -306,7 +308,7 @@ auto-includes = false
# Whether dist should create a Github Release or use an existing draft
create-release = true
# Which actions to run on pull requests
pr-run-mode = "skip"
pr-run-mode = "plan"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Which phase dist should use to create the GitHub release
@@ -334,7 +336,7 @@ install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
global = "depot-ubuntu-latest-4"
[workspace.metadata.dist.github-action-commits]
"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4
"actions/upload-artifact" = "ea165f8d65b6e75b540449e92b4886f43607fa02" # v4.6.2
"actions/download-artifact" = "95815c38cf2ff2164869cbab79da8d1f422bc89e" # v4.2.1
"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4
"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2
"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3

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.6/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.6/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.11.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.7/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.6
rev: v0.11.7
hooks:
# Run the linter.
- id: ruff

View File

@@ -2,16 +2,16 @@
## Basics
For now, we use our own [fork of mypy primer]. It can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
`mypy_primer` can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
```sh
uvx --from "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" mypy_primer -h
uvx --from "git+https://github.com/hauntsaninja/mypy_primer" mypy_primer -h
```
Alternatively, you can install the forked version of `mypy_primer` using:
```sh
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
uv tool install "git+https://github.com/hauntsaninja/mypy_primer"
```
and then run it using `uvx mypy_primer` or just `mypy_primer`, if your `PATH` is set up accordingly (see: [Tool executables]).
@@ -56,6 +56,5 @@ mypy_primer --repo /path/to/ruff --old origin/main --new my/local-branch …
Note that you might need to clean up `/tmp/mypy_primer` in order for this to work correctly.
[fork of mypy primer]: https://github.com/astral-sh/mypy_primer/tree/add-red-knot-support
[full list of ecosystem projects]: https://github.com/astral-sh/mypy_primer/blob/add-red-knot-support/mypy_primer/projects.py
[full list of ecosystem projects]: https://github.com/hauntsaninja/mypy_primer/blob/master/mypy_primer/projects.py
[tool executables]: https://docs.astral.sh/uv/concepts/tools/#tool-executables

View File

@@ -105,6 +105,19 @@ pub(crate) struct CheckCommand {
/// Watch files for changes and recheck files related to the changed files.
#[arg(long, short = 'W')]
pub(crate) watch: bool,
/// Respect file exclusions via `.gitignore` and other standard ignore files.
/// Use `--no-respect-gitignore` to disable.
#[arg(
long,
overrides_with("no_respect_ignore_files"),
help_heading = "File selection",
default_missing_value = "true",
num_args = 0..1
)]
respect_ignore_files: Option<bool>,
#[clap(long, overrides_with("respect_ignore_files"), hide = true)]
no_respect_ignore_files: bool,
}
impl CheckCommand {
@@ -120,6 +133,13 @@ impl CheckCommand {
)
};
// --no-respect-gitignore defaults to false and is set true by CLI flag. If passed, override config file
// Otherwise, only pass this through if explicitly set (don't default to anything here to
// make sure that doesn't take precedence over an explicitly-set config file value)
let respect_ignore_files = self
.no_respect_ignore_files
.then_some(false)
.or(self.respect_ignore_files);
Options {
environment: Some(EnvironmentOptions {
python_version: self
@@ -144,6 +164,7 @@ impl CheckCommand {
error_on_warning: self.error_on_warning,
}),
rules,
respect_ignore_files,
..Default::default()
}
}

View File

@@ -169,8 +169,12 @@ pub enum ExitStatus {
/// Checking was successful but there were errors.
Failure = 1,
/// Checking failed.
/// Checking failed due to an invocation error (e.g. the current directory no longer exists, incorrect CLI arguments, ...)
Error = 2,
/// Internal Red Knot error (panic, or any other error that isn't due to the user using the
/// program incorrectly or transient environment errors).
InternalError = 101,
}
impl Termination for ExitStatus {
@@ -246,11 +250,16 @@ impl MainLoop {
// Spawn a new task that checks the project. This needs to be done in a separate thread
// to prevent blocking the main loop here.
rayon::spawn(move || {
if let Ok(result) = db.check() {
// Send the result back to the main loop for printing.
sender
.send(MainLoopMessage::CheckCompleted { result, revision })
.unwrap();
match db.check() {
Ok(result) => {
// Send the result back to the main loop for printing.
sender
.send(MainLoopMessage::CheckCompleted { result, revision })
.unwrap();
}
Err(cancelled) => {
tracing::debug!("Check has been cancelled: {cancelled:?}");
}
}
});
}
@@ -264,12 +273,6 @@ impl MainLoop {
.format(terminal_settings.output_format)
.color(colored::control::SHOULD_COLORIZE.should_colorize());
let min_error_severity = if terminal_settings.error_on_warning {
Severity::Warning
} else {
Severity::Error
};
if check_revision == revision {
if db.project().files(db).is_empty() {
tracing::warn!("No python files found under the given path(s)");
@@ -284,13 +287,13 @@ impl MainLoop {
return Ok(ExitStatus::Success);
}
} else {
let mut failed = false;
let mut max_severity = Severity::Info;
let diagnostics_count = result.len();
for diagnostic in result {
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
failed |= diagnostic.severity() >= min_error_severity;
max_severity = max_severity.max(diagnostic.severity());
}
writeln!(
@@ -301,10 +304,17 @@ impl MainLoop {
)?;
if self.watcher.is_none() {
return Ok(if failed {
ExitStatus::Failure
} else {
ExitStatus::Success
return Ok(match max_severity {
Severity::Info => ExitStatus::Success,
Severity::Warning => {
if terminal_settings.error_on_warning {
ExitStatus::Failure
} else {
ExitStatus::Success
}
}
Severity::Error => ExitStatus::Failure,
Severity::Fatal => ExitStatus::InternalError,
});
}
}

View File

@@ -5,6 +5,116 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
#[test]
fn test_run_in_sub_directory() -> anyhow::Result<()> {
let case = TestCase::with_files([("test.py", "~"), ("subdir/nothing", "")])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> <temp_dir>/test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn test_include_hidden_files_by_default() -> anyhow::Result<()> {
let case = TestCase::with_files([(".test.py", "~")])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> .test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn test_respect_ignore_files() -> anyhow::Result<()> {
// First test that the default option works correctly (the file is skipped)
let case = TestCase::with_files([(".ignore", "test.py"), ("test.py", "~")])?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN No python files found under the given path(s)
");
// Test that we can set to false via CLI
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
// Test that we can set to false via config file
case.write_file("knot.toml", "respect-ignore-files = false")?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
// Ensure CLI takes precedence
case.write_file("knot.toml", "respect-ignore-files = true")?;
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
success: false
exit_code: 1
----- stdout -----
error: invalid-syntax
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
/// Specifying an option on the CLI should take precedence over the same setting in the
/// project's configuration. Here, this is tested for the Python version.
#[test]
@@ -32,12 +142,12 @@ fn config_override_python_version() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-attribute
--> <temp_dir>/test.py:5:7
error: lint:unresolved-attribute: Type `<module 'sys'>` has no attribute `last_exc`
--> test.py:5:7
|
4 | # Access `sys.last_exc` that was only added in Python 3.12
5 | print(sys.last_exc)
| ^^^^^^^^^^^^ Type `<module 'sys'>` has no attribute `last_exc`
| ^^^^^^^^^^^^
|
Found 1 diagnostic
@@ -84,7 +194,7 @@ fn config_override_python_platform() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
info: revealed-type: Revealed type
--> <temp_dir>/test.py:5:1
--> test.py:5:1
|
3 | from typing_extensions import reveal_type
4 |
@@ -102,7 +212,7 @@ fn config_override_python_platform() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
info: revealed-type: Revealed type
--> <temp_dir>/test.py:5:1
--> test.py:5:1
|
3 | from typing_extensions import reveal_type
4 |
@@ -165,11 +275,11 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
--> <temp_dir>/child/test.py:2:6
error: lint:unresolved-import: Cannot resolve import `utils`
--> test.py:2:6
|
2 | from utils import add
| ^^^^^ Cannot resolve import `utils`
| ^^^^^
3 |
4 | stat = add(10, 15)
|
@@ -265,22 +375,22 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero
--> <temp_dir>/test.py:2:5
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:7:7
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -301,11 +411,11 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/test.py:2:5
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
@@ -341,33 +451,33 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
--> <temp_dir>/test.py:2:8
error: lint:unresolved-import: Cannot resolve import `does_not_exit`
--> test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
| ^^^^^^^^^^^^^
3 |
4 | y = 4 / 0
|
error: lint:division-by-zero
--> <temp_dir>/test.py:4:5
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:9:7
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:9:7
|
7 | x = a
8 |
9 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 3 diagnostics
@@ -388,22 +498,22 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-import
--> <temp_dir>/test.py:2:8
warning: lint:unresolved-import: Cannot resolve import `does_not_exit`
--> test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
| ^^^^^^^^^^^^^
3 |
4 | y = 4 / 0
|
warning: lint:division-by-zero
--> <temp_dir>/test.py:4:5
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
@@ -439,22 +549,22 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero
--> <temp_dir>/test.py:2:5
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
--> <temp_dir>/test.py:7:7
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -476,11 +586,11 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/test.py:2:5
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
@@ -513,7 +623,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
warning: unknown-rule
--> <temp_dir>/pyproject.toml:3:1
--> pyproject.toml:3:1
|
2 | [tool.knot.rules]
3 | division-by-zer = "warn" # incorrect rule name
@@ -555,11 +665,11 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -585,7 +695,7 @@ fn exit_code_only_info() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
info: revealed-type: Revealed type
--> <temp_dir>/test.py:3:1
--> test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
@@ -615,7 +725,7 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
exit_code: 0
----- stdout -----
info: revealed-type: Revealed type
--> <temp_dir>/test.py:3:1
--> test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
@@ -638,11 +748,11 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -670,11 +780,11 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -699,20 +809,20 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -737,20 +847,20 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -775,20 +885,20 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:2:7
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -835,22 +945,22 @@ fn user_configuration() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -877,22 +987,22 @@ fn user_configuration() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
error: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -935,29 +1045,29 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
--> <temp_dir>/project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
error: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> project/main.py:2:5
|
2 | y = 4 / 0 # error: division-by-zero
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
|
error: lint:unresolved-import
--> <temp_dir>/project/other.py:2:6
error: lint:unresolved-import: Cannot resolve import `main2`
--> project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^ Cannot resolve import `main2`
| ^^^^^
3 |
4 | print(z)
|
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^
|
Found 3 diagnostics
----- stderr -----
@@ -972,22 +1082,22 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
--> <temp_dir>/project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
error: lint:unresolved-import
--> <temp_dir>/project/other.py:2:6
error: lint:unresolved-import: Cannot resolve import `main2`
--> project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^ Cannot resolve import `main2`
| ^^^^^
3 |
4 | print(z)
|
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^
|
Found 2 diagnostics
----- stderr -----
@@ -1042,8 +1152,8 @@ fn concise_diagnostics() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning[lint:unresolved-reference] <temp_dir>/test.py:2:7: Name `x` used when not defined
error[lint:non-subscriptable] <temp_dir>/test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
warning[lint:unresolved-reference] test.py:2:7: Name `x` used when not defined
error[lint:non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
Found 2 diagnostics
----- stderr -----
@@ -1076,7 +1186,7 @@ fn concise_revealed_type() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
info[revealed-type] <temp_dir>/test.py:5:1: Revealed type: `Literal["hello"]`
info[revealed-type] test.py:5:1: Revealed type: `Literal["hello"]`
Found 1 diagnostic
----- stderr -----

View File

@@ -272,9 +272,9 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> /main.py:2:19
--> main.py:2:19
|
2 | class Test: ...
| ^^^^
@@ -282,14 +282,14 @@ mod tests {
4 | ab = Test()
|
info: Source
--> /main.py:4:13
--> main.py:4:13
|
2 | class Test: ...
3 |
4 | ab = Test()
| ^^
|
"###);
");
}
#[test]
@@ -304,9 +304,9 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> /main.py:2:17
--> main.py:2:17
|
2 | def foo(a, b): ...
| ^^^
@@ -314,14 +314,14 @@ mod tests {
4 | ab = foo
|
info: Source
--> /main.py:6:13
--> main.py:6:13
|
4 | ab = foo
5 |
6 | ab
| ^^
|
"###);
");
}
#[test]
@@ -344,7 +344,7 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> /main.py:3:17
--> main.py:3:17
|
3 | def foo(a, b): ...
| ^^^
@@ -352,7 +352,7 @@ mod tests {
5 | def bar(a, b): ...
|
info: Source
--> /main.py:12:13
--> main.py:12:13
|
10 | a = bar
11 |
@@ -361,7 +361,7 @@ mod tests {
|
info: lint:goto-type-definition: Type definition
--> /main.py:5:17
--> main.py:5:17
|
3 | def foo(a, b): ...
4 |
@@ -371,7 +371,7 @@ mod tests {
7 | if random.choice():
|
info: Source
--> /main.py:12:13
--> main.py:12:13
|
10 | a = bar
11 |
@@ -395,13 +395,13 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> /lib.py:1:1
--> lib.py:1:1
|
1 | a = 10
| ^^^^^^
|
info: Source
--> /main.py:4:13
--> main.py:4:13
|
2 | import lib
3 |
@@ -433,7 +433,7 @@ mod tests {
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:13
--> main.py:4:13
|
2 | a: str = "test"
3 |
@@ -462,7 +462,7 @@ mod tests {
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:2:22
--> main.py:2:22
|
2 | a: str = "test"
| ^^^^^^
@@ -478,20 +478,20 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> /main.py:2:24
--> main.py:2:24
|
2 | type Alias[T: int = bool] = list[T]
| ^
|
info: Source
--> /main.py:2:46
--> main.py:2:46
|
2 | type Alias[T: int = bool] = list[T]
| ^
|
"###);
");
}
#[test]
@@ -544,7 +544,7 @@ mod tests {
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:18
--> main.py:4:18
|
2 | def test(a: str): ...
3 |
@@ -579,7 +579,7 @@ mod tests {
233 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...
|
info: Source
--> /main.py:4:18
--> main.py:4:18
|
2 | def test(a: str): ...
3 |
@@ -613,7 +613,7 @@ f(**kwargs<CURSOR>)
1088 | # Also multiprocessing.managers.SyncManager.dict()
|
info: Source
--> /main.py:6:5
--> main.py:6:5
|
4 | kwargs = { "name": "test"}
5 |
@@ -644,7 +644,7 @@ f(**kwargs<CURSOR>)
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:3:17
--> main.py:3:17
|
2 | def foo(a: str):
3 | a
@@ -666,23 +666,23 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> /main.py:2:19
--> main.py:2:19
|
2 | class X:
| ^
3 | def foo(a, b): ...
|
info: Source
--> /main.py:7:13
--> main.py:7:13
|
5 | x = X()
6 |
7 | x.foo()
| ^
|
"###);
");
}
#[test]
@@ -695,9 +695,9 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r###"
assert_snapshot!(test.goto_type_definition(), @r"
info: lint:goto-type-definition: Type definition
--> /main.py:2:17
--> main.py:2:17
|
2 | def foo(a, b): ...
| ^^^
@@ -705,14 +705,14 @@ f(**kwargs<CURSOR>)
4 | foo()
|
info: Source
--> /main.py:4:13
--> main.py:4:13
|
2 | def foo(a, b): ...
3 |
4 | foo()
| ^^^
|
"###);
");
}
#[test]
@@ -737,7 +737,7 @@ f(**kwargs<CURSOR>)
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:4:27
--> main.py:4:27
|
2 | def foo(a: str | None, b):
3 | if a is not None:
@@ -767,7 +767,7 @@ f(**kwargs<CURSOR>)
672 | def __bool__(self) -> Literal[False]: ...
|
info: Source
--> /main.py:3:17
--> main.py:3:17
|
2 | def foo(a: str | None, b):
3 | a
@@ -785,7 +785,7 @@ f(**kwargs<CURSOR>)
440 | def __new__(cls, object: object = ...) -> Self: ...
|
info: Source
--> /main.py:3:17
--> main.py:3:17
|
2 | def foo(a: str | None, b):
3 | a

View File

@@ -156,7 +156,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:9
--> main.py:4:9
|
2 | a = 10
3 |
@@ -192,7 +192,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:10:9
--> main.py:10:9
|
9 | foo = Foo()
10 | foo.a
@@ -214,7 +214,7 @@ mod tests {
"#,
);
assert_snapshot!(test.hover(), @r###"
assert_snapshot!(test.hover(), @r"
def foo(a, b) -> Unknown
---------------------------------------------
```text
@@ -222,7 +222,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:13
--> main.py:4:13
|
2 | def foo(a, b): ...
3 |
@@ -231,7 +231,7 @@ mod tests {
| |
| source
|
"###);
");
}
#[test]
@@ -251,7 +251,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:3:17
--> main.py:3:17
|
2 | def foo(a: int, b: int, c: int):
3 | a + b == c
@@ -282,7 +282,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:18
--> main.py:4:18
|
2 | def test(a: int): ...
3 |
@@ -312,7 +312,7 @@ mod tests {
"#,
);
assert_snapshot!(test.hover(), @r###"
assert_snapshot!(test.hover(), @r"
(def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown)
---------------------------------------------
```text
@@ -320,7 +320,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:12:13
--> main.py:12:13
|
10 | a = bar
11 |
@@ -329,7 +329,7 @@ mod tests {
| |
| source
|
"###);
");
}
#[test]
@@ -352,7 +352,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:13
--> main.py:4:13
|
2 | import lib
3 |
@@ -381,7 +381,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:2:46
--> main.py:2:46
|
2 | type Alias[T: int = bool] = list[T]
| ^- Cursor offset
@@ -407,7 +407,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:2:53
--> main.py:2:53
|
2 | type Alias[**P = [int, str]] = Callable[P, int]
| ^- Cursor offset
@@ -433,7 +433,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:2:43
--> main.py:2:43
|
2 | type Alias[*Ts = ()] = tuple[*Ts]
| ^^- Cursor offset
@@ -461,7 +461,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:3:13
--> main.py:3:13
|
2 | class Foo:
3 | a: int
@@ -490,7 +490,7 @@ mod tests {
```
---------------------------------------------
info: lint:hover: Hovered content is
--> /main.py:4:27
--> main.py:4:27
|
2 | def foo(a: str | None, b):
3 | if a is not None:

View File

@@ -0,0 +1,4 @@
from __future__ import annotations
def foo(a: foo()):
pass

View File

@@ -11,7 +11,7 @@ use red_knot_python_semantic::register_lints;
use red_knot_python_semantic::types::check_types;
use ruff_db::diagnostic::{
create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic,
DiagnosticId, Severity, Span,
DiagnosticId, Severity, Span, SubDiagnostic,
};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
@@ -20,8 +20,10 @@ use ruff_db::system::{SystemPath, SystemPathBuf};
use rustc_hash::FxHashSet;
use salsa::Durability;
use salsa::Setter;
use std::panic::{catch_unwind, AssertUnwindSafe, UnwindSafe};
use std::sync::Arc;
use thiserror::Error;
use tracing::error;
pub mod combine;
@@ -187,30 +189,66 @@ impl Project {
.map(IOErrorDiagnostic::to_diagnostic),
);
let result = Arc::new(std::sync::Mutex::new(diagnostics));
let inner_result = Arc::clone(&result);
let file_diagnostics = Arc::new(std::sync::Mutex::new(vec![]));
let db = db.clone();
let project_span = project_span.clone();
{
let file_diagnostics = Arc::clone(&file_diagnostics);
let db = db.clone();
let project_span = project_span.clone();
rayon::scope(move |scope| {
for file in &files {
let result = inner_result.clone();
let db = db.clone();
let project_span = project_span.clone();
rayon::scope(move |scope| {
for file in &files {
let result = Arc::clone(&file_diagnostics);
let db = db.clone();
let project_span = project_span.clone();
scope.spawn(move |_| {
let check_file_span =
tracing::debug_span!(parent: &project_span, "check_file", ?file);
let _entered = check_file_span.entered();
scope.spawn(move |_| {
let check_file_span =
tracing::debug_span!(parent: &project_span, "check_file", ?file);
let _entered = check_file_span.entered();
let file_diagnostics = check_file_impl(&db, file);
result.lock().unwrap().extend(file_diagnostics);
});
let file_diagnostics = check_file_impl(&db, file);
result.lock().unwrap().extend(file_diagnostics);
});
}
});
}
let mut file_diagnostics = Arc::into_inner(file_diagnostics)
.unwrap()
.into_inner()
.unwrap();
// We sort diagnostics in a way that keeps them in source order
// and grouped by file. After that, we fall back to severity
// (with fatal messages sorting before info messages) and then
// finally the diagnostic ID.
file_diagnostics.sort_by(|d1, d2| {
if let (Some(span1), Some(span2)) = (d1.primary_span(), d2.primary_span()) {
let order = span1
.file()
.path(db)
.as_str()
.cmp(span2.file().path(db).as_str());
if order.is_ne() {
return order;
}
if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) {
let order = range1.start().cmp(&range2.start());
if order.is_ne() {
return order;
}
}
}
// Reverse so that, e.g., Fatal sorts before Info.
let order = d1.severity().cmp(&d2.severity()).reverse();
if order.is_ne() {
return order;
}
d1.id().cmp(&d2.id())
});
Arc::into_inner(result).unwrap().into_inner().unwrap()
diagnostics.extend(file_diagnostics);
diagnostics
}
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
@@ -314,20 +352,23 @@ impl Project {
/// * It has a [`SystemPath`] and belongs to a package's `src` files
/// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath)
pub fn is_file_open(self, db: &dyn Db, file: File) -> bool {
let path = file.path(db);
// Try to return early to avoid adding a dependency on `open_files` or `file_set` which
// both have a durability of `LOW`.
if path.is_vendored_path() {
return false;
}
if let Some(open_files) = self.open_files(db) {
open_files.contains(&file)
} else if file.path(db).is_system_path() {
self.contains_file(db, file)
self.files(db).contains(&file)
} else {
file.path(db).is_system_virtual_path()
}
}
/// Returns `true` if `file` is a first-party file part of this package.
pub fn contains_file(self, db: &dyn Db, file: File) -> bool {
self.files(db).contains(&file)
}
#[tracing::instrument(level = "debug", skip(self, db))]
pub fn remove_file(self, db: &mut dyn Db, file: File) {
tracing::debug!(
@@ -432,7 +473,16 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
);
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
{
let db = AssertUnwindSafe(db);
match catch(&**db, file, || check_types(db.upcast(), file)) {
Ok(Some(type_check_diagnostics)) => {
diagnostics.extend(type_check_diagnostics.into_iter().cloned());
}
Ok(None) => {}
Err(diagnostic) => diagnostics.push(diagnostic),
}
}
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
@@ -523,6 +573,45 @@ enum IOErrorKind {
SourceText(#[from] SourceTextError),
}
fn catch<F, R>(db: &dyn Db, file: File, f: F) -> Result<Option<R>, Diagnostic>
where
F: FnOnce() -> R + UnwindSafe,
{
match catch_unwind(|| {
// Ignore salsa errors
salsa::Cancelled::catch(f).ok()
}) {
Ok(result) => Ok(result),
Err(error) => {
let payload = if let Some(s) = error.downcast_ref::<&str>() {
Some((*s).to_string())
} else {
error.downcast_ref::<String>().cloned()
};
let message = if let Some(payload) = payload {
format!(
"Panicked while checking `{file}`: `{payload}`",
file = file.path(db)
)
} else {
format!("Panicked while checking `{file}`", file = { file.path(db) })
};
let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message);
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
"This indicates a bug in Red Knot.",
));
let report_message = "If you could open an issue at https://github.com/astral-sh/ruff/issues/new?title=%5Bred-knot%5D:%20panic we'd be very appreciative!";
diagnostic.sub(SubDiagnostic::new(Severity::Info, report_message));
Err(diagnostic)
}
}
}
#[cfg(test)]
mod tests {
use crate::db::tests::TestDb;

View File

@@ -32,6 +32,9 @@ pub struct Options {
#[serde(skip_serializing_if = "Option::is_none")]
pub terminal: Option<TerminalOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub respect_ignore_files: Option<bool>,
}
impl Options {
@@ -133,7 +136,7 @@ impl Options {
pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec<OptionDiagnostic>) {
let (rules, diagnostics) = self.to_rule_selection(db);
let mut settings = Settings::new(rules);
let mut settings = Settings::new(rules, self.respect_ignore_files);
if let Some(terminal) = self.terminal.as_ref() {
settings.set_terminal(TerminalSettings {

View File

@@ -21,13 +21,16 @@ pub struct Settings {
rules: Arc<RuleSelection>,
terminal: TerminalSettings,
respect_ignore_files: bool,
}
impl Settings {
pub fn new(rules: RuleSelection) -> Self {
pub fn new(rules: RuleSelection, respect_ignore_files: Option<bool>) -> Self {
Self {
rules: Arc::new(rules),
terminal: TerminalSettings::default(),
respect_ignore_files: respect_ignore_files.unwrap_or(true),
}
}
@@ -35,6 +38,10 @@ impl Settings {
&self.rules
}
pub fn respect_ignore_files(&self) -> bool {
self.respect_ignore_files
}
pub fn to_rules(&self) -> Arc<RuleSelection> {
self.rules.clone()
}

View File

@@ -129,7 +129,11 @@ impl<'a> ProjectFilesWalker<'a> {
{
let mut paths = paths.into_iter();
let mut walker = db.system().walk_directory(paths.next()?.as_ref());
let mut walker = db
.system()
.walk_directory(paths.next()?.as_ref())
.standard_filters(db.project().settings(db).respect_ignore_files())
.ignore_hidden(false);
for path in paths {
walker = walker.add(path);

View File

@@ -50,10 +50,9 @@ y: Any = "not an Any" # error: [invalid-assignment]
The spec allows you to define subclasses of `Any`.
TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The
assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment
to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore
assignable to `int`.
`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
```py
from typing import Any
@@ -63,13 +62,33 @@ class Subclass(Any): ...
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
x: Subclass = 1 # error: [invalid-assignment]
# TODO: no diagnostic
y: int = Subclass() # error: [invalid-assignment]
y: int = Subclass()
def _(s: Subclass):
reveal_type(s) # revealed: Subclass
```
`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
be a subclass of `FinalClass`:
```py
from typing import final
@final
class FinalClass: ...
f: FinalClass = Subclass() # error: [invalid-assignment]
```
A use case where this comes up is with mocking libraries, where the mock object should be assignable
to any type:
```py
from unittest.mock import MagicMock
x: int = MagicMock()
```
## Invalid
`Any` cannot be parameterized:

View File

@@ -225,14 +225,16 @@ Using `Concatenate` as the first argument to `Callable`:
from typing_extensions import Callable, Concatenate
def _(c: Callable[Concatenate[int, str, ...], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
```
And, as one of the parameter types:
```py
def _(c: Callable[[Concatenate[int, str, ...], int], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
```
## Using `typing.ParamSpec`
@@ -276,7 +278,8 @@ from typing_extensions import Callable, TypeVarTuple
Ts = TypeVarTuple("Ts")
def _(c: Callable[[int, *Ts], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
```
And, using the legacy syntax using `Unpack`:
@@ -285,7 +288,8 @@ And, using the legacy syntax using `Unpack`:
from typing_extensions import Unpack
def _(c: Callable[[int, Unpack[Ts]], int]):
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> int
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
```
## Member lookup

View File

@@ -56,40 +56,41 @@ def _(
def bar() -> None:
return None
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(unknown type subscript)
async def outer(): # avoid unrelated syntax errors on yield, yield from, and await
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(unknown type subscript)
```
## Invalid Collection based AST nodes

View File

@@ -38,8 +38,12 @@ bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
```py
from typing_extensions import LiteralString
a: LiteralString[str] # error: [invalid-type-form]
b: LiteralString["foo"] # error: [invalid-type-form]
# error: [invalid-type-form]
a: LiteralString[str]
# error: [invalid-type-form]
# error: [unresolved-reference] "Name `foo` used when not defined"
b: LiteralString["foo"]
```
### As a base class

View File

@@ -106,13 +106,13 @@ reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]]
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], @Todo(GenericAlias instance), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(CounterSubclass.__mro__)
class DefaultDictSubclass(typing.DefaultDict): ...
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]]
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], @Todo(GenericAlias instance), Literal[object]]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
@@ -124,6 +124,6 @@ reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]]
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], @Todo(GenericAlias instance), Literal[object]]
reveal_type(OrderedDictSubclass.__mro__)
```

View File

@@ -89,9 +89,12 @@ python-version = "3.12"
Some of these are not subscriptable:
```py
from typing_extensions import Self, TypeAlias
from typing_extensions import Self, TypeAlias, TypeVar
X: TypeAlias[T] = int # error: [invalid-type-form]
T = TypeVar("T")
# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter"
X: TypeAlias[T] = int
class Foo[T]:
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"

View File

@@ -11,8 +11,6 @@ from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict
X: Final = 42
Y: Final[int] = 42
# TODO: `TypedDict` is actually valid as a base
# error: [invalid-base]
class Bar(TypedDict):
x: Required[int]
y: NotRequired[str]

View File

@@ -50,11 +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: int
reveal_type(x**2) # revealed: (int & Any) | (int & float & Any)
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(2**x) # revealed: int
reveal_type(2**x) # revealed: (int & Any) | (int & float & Any)
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(x**x) # revealed: int
reveal_type(x**x) # revealed: (int & Any) | (int & float & Any)
```
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but

View File

@@ -292,3 +292,66 @@ reveal_type(a) # revealed: Unknown
# Modifications allowed in this case:
a = None
```
## In stub files
In stub files, we have a minor modification to the rules above: we do not union with `Unknown` for
undeclared symbols.
### Undeclared and bound
`mod.pyi`:
```pyi
MyInt = int
class C:
MyStr = str
```
```py
from mod import MyInt, C
reveal_type(MyInt) # revealed: Literal[int]
reveal_type(C.MyStr) # revealed: Literal[str]
```
### Undeclared and possibly unbound
`mod.pyi`:
```pyi
def flag() -> bool:
return True
if flag():
MyInt = int
class C:
MyStr = str
```
```py
# error: [possibly-unbound-import]
# error: [possibly-unbound-import]
from mod import MyInt, C
reveal_type(MyInt) # revealed: Literal[int]
reveal_type(C.MyStr) # revealed: Literal[str]
```
### Undeclared and unbound
`mod.pyi`:
```pyi
if False:
MyInt = int
```
```py
# error: [unresolved-import]
from mod import MyInt
reveal_type(MyInt) # revealed: Unknown
```

View File

@@ -162,6 +162,44 @@ def _(flag: bool):
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
```
## Unions with literals and negations
```py
from typing import Literal
from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[AlwaysFalsy]))
static_assert(is_subtype_of(Not[AlwaysFalsy], Literal["", "a"] | Not[AlwaysFalsy]))
static_assert(is_subtype_of(Literal["a", ""], Not[AlwaysFalsy] | Literal["a", ""]))
static_assert(is_subtype_of(Not[AlwaysFalsy], Not[AlwaysFalsy] | Literal["a", ""]))
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[Literal[""]]))
static_assert(is_subtype_of(Not[Literal[""]], Literal["a", ""] | Not[Literal[""]]))
static_assert(is_subtype_of(Literal["a", ""], Not[Literal[""]] | Literal["a", ""]))
static_assert(is_subtype_of(Not[Literal[""]], Not[Literal[""]] | Literal["a", ""]))
def _(
a: Literal["a", ""] | Not[AlwaysFalsy],
b: Literal["a", ""] | Not[Literal[""]],
c: Literal[""] | Not[Literal[""]],
d: Not[Literal[""]] | Literal[""],
e: Literal["a"] | Not[Literal["a"]],
f: Literal[b"b"] | Not[Literal[b"b"]],
g: Not[Literal[b"b"]] | Literal[b"b"],
h: Literal[42] | Not[Literal[42]],
i: Not[Literal[42]] | Literal[42],
):
reveal_type(a) # revealed: Literal[""] | ~AlwaysFalsy
reveal_type(b) # revealed: object
reveal_type(c) # revealed: object
reveal_type(d) # revealed: object
reveal_type(e) # revealed: object
reveal_type(f) # revealed: object
reveal_type(g) # revealed: object
reveal_type(h) # revealed: object
reveal_type(i) # revealed: object
```
## Cannot use an argument as both a value and a type form
```py

View File

@@ -13,7 +13,7 @@ reveal_type(1 is not 1) # revealed: bool
reveal_type(1 is 2) # revealed: Literal[False]
reveal_type(1 is not 7) # revealed: Literal[True]
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True]
```
## Integer instance

View File

@@ -37,7 +37,7 @@ class C:
return self
x = A() < B() < C()
reveal_type(x) # revealed: A & ~AlwaysTruthy | B
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B
y = 0 < 1 < A() < 3
reveal_type(y) # revealed: Literal[False] | A

View File

@@ -127,8 +127,9 @@ class AsyncIterable:
def __aiter__(self) -> AsyncIterator:
return AsyncIterator()
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in AsyncIterable()]
async def _():
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in AsyncIterable()]
```
### Invalid async comprehension
@@ -145,6 +146,7 @@ class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
async def _():
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
```

View File

@@ -42,6 +42,6 @@ def _(flag: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
3 if NotBoolable() else 4
```

View File

@@ -154,10 +154,10 @@ def _(flag: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
if NotBoolable():
...
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
elif NotBoolable():
...
```

View File

@@ -292,7 +292,7 @@ class NotBoolable:
def _(target: int, flag: NotBoolable):
y = 1
match target:
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
case 1 if flag:
y = 2
case 2:

View File

@@ -0,0 +1,293 @@
# `typing.dataclass_transform`
```toml
[environment]
python-version = "3.12"
```
`dataclass_transform` is a decorator that can be used to let type checkers know that a function,
class, or metaclass is a `dataclass`-like construct.
## Basic example
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
# modify cls
return cls
@my_dataclass
class Person:
name: str
age: int | None = None
Person("Alice", 20)
Person("Bob", None)
Person("Bob")
# error: [missing-argument]
Person()
```
## Decorating decorators that take parameters themselves
If we want our `dataclass`-like decorator to also take parameters, that is also possible:
```py
from typing_extensions import dataclass_transform, Callable
@dataclass_transform()
def versioned_class[T](*, version: int = 1):
def decorator(cls):
# modify cls
return cls
return decorator
@versioned_class(version=2)
class Person:
name: str
age: int | None = None
Person("Alice", 20)
# error: [missing-argument]
Person()
```
We properly type-check the arguments to the decorator:
```py
from typing_extensions import dataclass_transform, Callable
# error: [invalid-argument-type]
@versioned_class(version="a string")
class C:
name: str
```
## Types of decorators
The examples from this section are straight from the Python documentation on
[`typing.dataclass_transform`].
### Decorating a decorator function
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def create_model[T](cls: type[T]) -> type[T]:
...
return cls
@create_model
class CustomerModel:
id: int
name: str
CustomerModel(id=1, name="Test")
```
### Decorating a metaclass
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
class ModelMeta(type): ...
class ModelBase(metaclass=ModelMeta): ...
class CustomerModel(ModelBase):
id: int
name: str
CustomerModel(id=1, name="Test")
# error: [missing-argument]
CustomerModel()
```
### Decorating a base class
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
class ModelBase: ...
class CustomerModel(ModelBase):
id: int
name: str
# TODO: this is not supported yet
# error: [unknown-argument]
# error: [unknown-argument]
CustomerModel(id=1, name="Test")
```
## Arguments to `dataclass_transform`
### `eq_default`
`eq=True/False` does not have a observable effect (apart from a minor change regarding whether
`other` is positional-only or not, which is not modelled at the moment).
### `order_default`
The `order_default` argument controls whether methods such as `__lt__` are generated by default.
This can be overwritten using the `order` argument to the custom decorator:
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def normal(*, order: bool = False):
raise NotImplementedError
@dataclass_transform(order_default=False)
def order_default_false(*, order: bool = False):
raise NotImplementedError
@dataclass_transform(order_default=True)
def order_default_true(*, order: bool = True):
raise NotImplementedError
@normal
class Normal:
inner: int
Normal(1) < Normal(2) # error: [unsupported-operator]
@normal(order=True)
class NormalOverwritten:
inner: int
NormalOverwritten(1) < NormalOverwritten(2)
@order_default_false
class OrderFalse:
inner: int
OrderFalse(1) < OrderFalse(2) # error: [unsupported-operator]
@order_default_false(order=True)
class OrderFalseOverwritten:
inner: int
OrderFalseOverwritten(1) < OrderFalseOverwritten(2)
@order_default_true
class OrderTrue:
inner: int
OrderTrue(1) < OrderTrue(2)
@order_default_true(order=False)
class OrderTrueOverwritten:
inner: int
# error: [unsupported-operator]
OrderTrueOverwritten(1) < OrderTrueOverwritten(2)
```
### `kw_only_default`
To do
### `field_specifiers`
To do
## Overloaded dataclass-like decorators
In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the
implementation, or to *one* of the overloads.
### Applying `dataclass_transform` to the implementation
```py
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
T = TypeVar("T", bound=type)
@overload
def versioned_class(
cls: T,
*,
version: int = 1,
) -> T: ...
@overload
def versioned_class(
*,
version: int = 1,
) -> Callable[[T], T]: ...
@dataclass_transform()
def versioned_class(
cls: T | None = None,
*,
version: int = 1,
) -> T | Callable[[T], T]:
raise NotImplementedError
@versioned_class
class D1:
x: str
@versioned_class(version=2)
class D2:
x: str
D1("a")
D2("a")
D1(1.2) # error: [invalid-argument-type]
D2(1.2) # error: [invalid-argument-type]
```
### Applying `dataclass_transform` to an overload
```py
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
T = TypeVar("T", bound=type)
@overload
@dataclass_transform()
def versioned_class(
cls: T,
*,
version: int = 1,
) -> T: ...
@overload
def versioned_class(
*,
version: int = 1,
) -> Callable[[T], T]: ...
def versioned_class(
cls: T | None = None,
*,
version: int = 1,
) -> T | Callable[[T], T]:
raise NotImplementedError
@versioned_class
class D1:
x: str
@versioned_class(version=2)
class D2:
x: str
# TODO: these should not be errors
D1("a") # error: [too-many-positional-arguments]
D2("a") # error: [too-many-positional-arguments]
# TODO: these should be invalid-argument-type errors
D1(1.2) # error: [too-many-positional-arguments]
D2(1.2) # error: [too-many-positional-arguments]
```
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform

View File

@@ -570,7 +570,7 @@ class ConvertToLength:
class C:
converter: ConvertToLength = ConvertToLength()
reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None
reveal_type(C.__init__) # revealed: (converter: str = Never) -> None
c = C("abc")
reveal_type(c.converter) # revealed: int
@@ -689,7 +689,7 @@ from dataclasses import dataclass
dataclass_with_order = dataclass(order=True)
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclasses.dataclass>
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclass-like function>
@dataclass_with_order
class C:

View File

@@ -459,7 +459,7 @@ class Descriptor:
class C:
d: Descriptor = Descriptor()
reveal_type(C.d) # revealed: Literal["called on class object"]
reveal_type(C.d) # revealed: Never
reveal_type(C().d) # revealed: Literal["called on instance"]
```

View File

@@ -0,0 +1,165 @@
# Semantic syntax error diagnostics
## `async` comprehensions in synchronous comprehensions
### Python 3.10
<!-- snapshot-diagnostics -->
Before Python 3.11, `async` comprehensions could not be used within outer sync comprehensions, even
within an `async` function ([CPython issue](https://github.com/python/cpython/issues/77527)):
```toml
[environment]
python-version = "3.10"
```
```py
async def elements(n):
yield n
async def f():
# error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)"
return {n: [x async for x in elements(n)] for n in range(3)}
```
If all of the comprehensions are `async`, on the other hand, the code was still valid:
```py
async def test():
return [[x async for x in elements(n)] async for n in range(3)]
```
These are a couple of tricky but valid cases to check that nested scope handling is wired up
correctly in the `SemanticSyntaxContext` trait:
```py
async def f():
[x for x in [1]] and [x async for x in elements(1)]
async def f():
def g():
pass
[x async for x in elements(1)]
```
### Python 3.11
All of these same examples are valid after Python 3.11:
```toml
[environment]
python-version = "3.11"
```
```py
async def elements(n):
yield n
async def f():
return {n: [x async for x in elements(n)] for n in range(3)}
```
## Late `__future__` import
```py
from collections import namedtuple
# error: [invalid-syntax] "__future__ imports must be at the top of the file"
from __future__ import print_function
```
## Invalid annotation
This one might be a bit redundant with the `invalid-type-form` error.
```toml
[environment]
python-version = "3.12"
```
```py
from __future__ import annotations
# error: [invalid-type-form] "Named expressions are not allowed in type expressions"
# error: [invalid-syntax] "named expression cannot be used within a type annotation"
def f() -> (y := 3): ...
```
## Duplicate `match` key
```toml
[environment]
python-version = "3.10"
```
```py
match 2:
# error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`"
case {"x": 1, "x": 2}:
...
```
## `return`, `yield`, `yield from`, and `await` outside function
```py
# error: [invalid-syntax] "`return` statement outside of a function"
return
# error: [invalid-syntax] "`yield` statement outside of a function"
yield
# error: [invalid-syntax] "`yield from` statement outside of a function"
yield from []
# error: [invalid-syntax] "`await` statement outside of a function"
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
def f():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
```
Generators are evaluated lazily, so `await` is allowed, even outside of a function.
```py
async def g():
yield 1
(x async for x in g())
```
## `await` outside async function
This error includes `await`, `async for`, `async with`, and `async` comprehensions.
```python
async def elements(n):
yield n
def _():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
# error: [invalid-syntax] "`async for` outside of an asynchronous function"
async for _ in elements(1):
...
# error: [invalid-syntax] "`async with` outside of an asynchronous function"
async with elements(1) as x:
...
# error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)"
# error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function"
[x async for x in elements(1)]
```
## Load before `global` declaration
This should be an error, but it's not yet.
TODO implement `SemanticSyntaxContext::global`
```py
def f():
x = 1
global x
```

View File

@@ -0,0 +1,19 @@
# Shadowing
<!-- snapshot-diagnostics -->
## Implicit class shadowing
```py
class C: ...
C = 1 # error: [invalid-assignment]
```
## Implicit function shadowing
```py
def f(): ...
f = 1 # error: [invalid-assignment]
```

View File

@@ -8,14 +8,20 @@
a, b = 1 # error: [not-iterable]
```
## Too many values to unpack
## Exactly too many values to unpack
```py
a, b = (1, 2, 3) # error: [invalid-assignment]
```
## Too few values to unpack
## Exactly too few values to unpack
```py
a, b = (1,) # error: [invalid-assignment]
```
## Too few values to unpack
```py
[a, *b, c, d] = (1, 2) # error: [invalid-assignment]
```

View File

@@ -0,0 +1,61 @@
<!-- snapshot-diagnostics -->
# Different ways that `unsupported-bool-conversion` can occur
## Has a `__bool__` method, but has incorrect parameters
```py
class NotBoolable:
def __bool__(self, foo):
return False
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Has a `__bool__` method, but has an incorrect return type
```py
class NotBoolable:
def __bool__(self) -> str:
return "wat"
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Has a `__bool__` attribute, but it's not callable
```py
class NotBoolable:
__bool__: int = 3
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Part of a union where at least one member has incorrect `__bool__` method
```py
class NotBoolable1:
def __bool__(self) -> str:
return "wat"
class NotBoolable2:
pass
class NotBoolable3:
__bool__: int = 3
def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3:
return NotBoolable2()
# error: [unsupported-bool-conversion]
10 and get() and True
```

View File

@@ -4,6 +4,6 @@
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
assert NotBoolable()
```

View File

@@ -10,8 +10,8 @@ def _(foo: str):
reveal_type(False or "z") # revealed: Literal["z"]
reveal_type(False or True) # revealed: Literal[True]
reveal_type(False or False) # revealed: Literal[False]
reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False]
reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True]
reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False]
reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True]
```
## AND
@@ -20,8 +20,8 @@ def _(foo: str):
def _(foo: str):
reveal_type(True and False) # revealed: Literal[False]
reveal_type(False and True) # revealed: Literal[False]
reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False]
reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True]
reveal_type(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False]
reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | Literal[True]
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
reveal_type("x" and "y" and "") # revealed: Literal[""]
reveal_type("" and "y") # revealed: Literal[""]
@@ -123,7 +123,7 @@ if NotBoolable():
class NotBoolable:
__bool__: None = None
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
if NotBoolable():
...
```
@@ -135,7 +135,7 @@ def test(cond: bool):
class NotBoolable:
__bool__: int | None = None if cond else 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
if NotBoolable():
...
```
@@ -149,7 +149,7 @@ def test(cond: bool):
a = 10 if cond else NotBoolable()
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`"
if a:
...
```

View File

@@ -203,7 +203,7 @@ from typing import TypeVar
T = TypeVar("T")
# TODO: `invalid-return-type` error should be emitted
# error: [invalid-return-type]
def m(x: T) -> T: ...
```

View File

@@ -232,21 +232,11 @@ TODO: These do not currently work yet, because we don't correctly model the nest
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]
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
reveal_type(C(1, True)) # revealed: C[Literal[1]]
# 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]`"
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```
@@ -285,14 +275,16 @@ c: C[int] = C[int]()
reveal_type(c.method("string")) # revealed: Literal["string"]
```
## Cyclic class definition
## Cyclic class definitions
### F-bounded quantification
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
#### In a stub file
`stub.pyi`:
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
```pyi
class Base[T]: ...
@@ -301,9 +293,9 @@ class Sub(Base[Sub]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
A similar case can work in a non-stub file, if forward references are stringified:
#### With string forward references
`string_annotation.py`:
A similar case can work in a non-stub file, if forward references are stringified:
```py
class Base[T]: ...
@@ -312,9 +304,9 @@ class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
In a non-stub file, without stringified forward references, this raises a `NameError`:
#### Without string forward references
`bare_annotation.py`:
In a non-stub file, without stringified forward references, this raises a `NameError`:
```py
class Base[T]: ...
@@ -323,13 +315,23 @@ class Base[T]: ...
class Sub(Base[Sub]): ...
```
## Another cyclic case
### Cyclic inheritance as a generic parameter
```pyi
# TODO no error (generics)
# error: [invalid-base]
class Derived[T](list[Derived[T]]): ...
```
### Direct cyclic inheritance
Inheritance that would result in a cyclic MRO is detected as an error.
```py
# error: [cyclic-class-definition]
class C[T](C): ...
# error: [cyclic-class-definition]
class D[T](D[int]): ...
```
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

View File

@@ -19,6 +19,9 @@ in newer Python releases.
from typing import TypeVar
T = TypeVar("T")
reveal_type(type(T)) # revealed: Literal[TypeVar]
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```
### Directly assigned to a variable
@@ -29,7 +32,12 @@ T = TypeVar("T")
```py
from typing import TypeVar
# TODO: error
T = TypeVar("T")
# TODO: no error
# error: [invalid-legacy-type-variable]
U: TypeVar = TypeVar("U")
# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
TestList = list[TypeVar("W")]
```
@@ -40,7 +48,7 @@ TestList = list[TypeVar("W")]
```py
from typing import TypeVar
# TODO: error
# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)"
T = TypeVar("Q")
```
@@ -57,6 +65,52 @@ T = TypeVar("T")
T = TypeVar("T")
```
### Type variables with a default
Note that the `__default__` property is only available in Python ≥3.13.
```toml
[environment]
python-version = "3.13"
```
```py
from typing import TypeVar
T = TypeVar("T", default=int)
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
S = TypeVar("S")
reveal_type(S.__default__) # revealed: NoDefault
```
### Type variables with an upper bound
```py
from typing import TypeVar
T = TypeVar("T", bound=int)
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
S = TypeVar("S")
reveal_type(S.__bound__) # revealed: None
```
### Type variables with constraints
```py
from typing import TypeVar
T = TypeVar("T", int, str)
reveal_type(T.__constraints__) # revealed: tuple[int, str]
S = TypeVar("S")
reveal_type(S.__constraints__) # revealed: tuple[()]
```
### Cannot have only one constraint
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should

View File

@@ -17,10 +17,51 @@ instances of `typing.TypeVar`, just like legacy type variables.
```py
def f[T]():
reveal_type(type(T)) # revealed: Literal[TypeVar]
reveal_type(T) # revealed: T
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```
### Type variables with a default
Note that the `__default__` property is only available in Python ≥3.13.
```toml
[environment]
python-version = "3.13"
```
```py
def f[T = int]():
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
def g[S]():
reveal_type(S.__default__) # revealed: NoDefault
```
### Type variables with an upper bound
```py
def f[T: int]():
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
def g[S]():
reveal_type(S.__bound__) # revealed: None
```
### Type variables with constraints
```py
def f[T: (int, str)]():
reveal_type(T.__constraints__) # revealed: tuple[int, str]
reveal_type(T.__bound__) # revealed: None
def g[S]():
reveal_type(S.__constraints__) # revealed: tuple[()]
```
### Cannot have only one constraint
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should

View File

@@ -142,8 +142,7 @@ class Legacy(Generic[T]):
return y
legacy: Legacy[int] = Legacy()
# TODO: revealed: str
reveal_type(legacy.m(1, "string")) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions)
reveal_type(legacy.m(1, "string")) # revealed: Literal["string"]
```
With PEP 695 syntax, it is clearer that the method uses a separate typevar:

View File

@@ -189,7 +189,7 @@ match 42:
...
case [O]:
...
case P | Q:
case P | Q: # error: [invalid-syntax] "name capture `P` makes remaining patterns unreachable"
...
case object(foo=R):
...
@@ -289,7 +289,7 @@ match 42:
...
case [D]:
...
case E | F:
case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable"
...
case object(foo=G):
...
@@ -357,7 +357,7 @@ match 42:
...
case [D]:
...
case E | F:
case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable"
...
case object(foo=G):
...

View File

@@ -191,9 +191,9 @@ def _(
i2: Intersection[P | Q | R, S],
i3: Intersection[P | Q, R | S],
) -> None:
reveal_type(i1) # revealed: P & Q | P & R | P & S
reveal_type(i2) # revealed: P & S | Q & S | R & S
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
reveal_type(i1) # revealed: (P & Q) | (P & R) | (P & S)
reveal_type(i2) # revealed: (P & S) | (Q & S) | (R & S)
reveal_type(i3) # revealed: (P & R) | (Q & R) | (P & S) | (Q & S)
def simplifications_for_same_elements(
i1: Intersection[P, Q | P],
@@ -216,7 +216,7 @@ def simplifications_for_same_elements(
# = P & Q | P & R | Q | Q & R
# = Q | P & R
# (again, because Q is a supertype of P & Q and of Q & R)
reveal_type(i3) # revealed: Q | P & R
reveal_type(i3) # revealed: Q | (P & R)
# (P | Q) & (P | Q)
# = P & P | P & Q | Q & P | Q & Q

View File

@@ -286,7 +286,7 @@ class Test:
def __iter__(self) -> TestIter | int:
return TestIter()
# error: [not-iterable] "Object of type `Test` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method"
# error: [not-iterable] "Object of type `Test` may not be iterable"
for x in Test():
reveal_type(x) # revealed: int
```
@@ -316,12 +316,12 @@ def _(flag: bool):
else:
__iter__: None = None
# error: [not-iterable] "Object of type `Iterable1` may not be iterable because its `__iter__` attribute (with type `CustomCallable`) may not be callable"
# error: [not-iterable] "Object of type `Iterable1` may not be iterable"
for x in Iterable1():
# 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 Iterable2.__iter__() -> Iterator) | None`) may not be callable"
# error: [not-iterable] "Object of type `Iterable2` may not be iterable"
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown
@@ -376,7 +376,7 @@ def _(flag: bool):
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable] "Object of type `Iterable` may not be iterable because its `__iter__` method returns an object of type `Iterator`, which may not have a `__next__` method"
# error: [not-iterable] "Object of type `Iterable` may not be iterable"
for x in Iterable():
reveal_type(x) # revealed: int
```
@@ -461,7 +461,7 @@ def _(flag: bool):
return Iterator()
__getitem__: None = None
# error: [not-iterable] "Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable"
# error: [not-iterable] "Object of type `Iterable` may not be iterable"
for x in Iterable():
reveal_type(x) # revealed: int
```

View File

@@ -123,7 +123,7 @@ def _(flag: bool, flag2: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
while NotBoolable():
...
```

View File

@@ -22,6 +22,7 @@ We can then place custom stub files in `/typeshed/stdlib`, for example:
`/typeshed/stdlib/builtins.pyi`:
```pyi
class object: ...
class BuiltinClass: ...
builtin_symbol: BuiltinClass

View File

@@ -53,6 +53,25 @@ class B(A): ...
reveal_type(B.__class__) # revealed: Literal[M]
```
## Linear inheritance with PEP 695 generic class
The same is true if the base with the metaclass is a generic class.
```toml
[environment]
python-version = "3.13"
```
```py
class M(type): ...
class A[T](metaclass=M): ...
class B(A): ...
class C(A[int]): ...
reveal_type(B.__class__) # revealed: Literal[M]
reveal_type(C.__class__) # revealed: Literal[M]
```
## Conflict (1)
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its

View File

@@ -191,8 +191,8 @@ reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Lit
## `__bases__` includes a `Union`
We don't support union types in a class's bases; a base must resolve to a single `ClassLiteralType`.
If we find a union type in a class's bases, we infer the class's `__mro__` as being
We don't support union types in a class's bases; a base must resolve to a single `ClassType`. If we
find a union type in a class's bases, we infer the class's `__mro__` as being
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
```py

View File

@@ -29,7 +29,7 @@ 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]
reveal_type(y) # revealed: Literal[2]
```
## `assert` with `isinstance`

View File

@@ -10,7 +10,7 @@ def _(x: A | B):
if isinstance(x, A) and isinstance(x, B):
reveal_type(x) # revealed: A & B
else:
reveal_type(x) # revealed: B & ~A | A & ~B
reveal_type(x) # revealed: (B & ~A) | (A & ~B)
```
## Arms might not add narrowing constraints
@@ -131,8 +131,8 @@ def _(x: A | B | C, y: A | B | C):
# The same for `y`
reveal_type(y) # revealed: A | B | C
else:
reveal_type(x) # revealed: B & ~A | C & ~A
reveal_type(y) # revealed: B & ~A | C & ~A
reveal_type(x) # revealed: (B & ~A) | (C & ~A)
reveal_type(y) # revealed: (B & ~A) | (C & ~A)
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
@@ -155,7 +155,7 @@ def _(x: A | B | C):
reveal_type(x) # revealed: B & ~C
else:
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
reveal_type(x) # revealed: A & ~B | C
reveal_type(x) # revealed: (A & ~B) | C
```
## mixing `or` and `not`
@@ -167,7 +167,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | A & ~C
reveal_type(x) # revealed: B | (A & ~C)
else:
reveal_type(x) # revealed: C & ~B
```
@@ -181,7 +181,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
reveal_type(x) # revealed: A | B & ~C
reveal_type(x) # revealed: A | (B & ~C)
else:
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
reveal_type(x) # revealed: C & ~A
@@ -197,7 +197,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
# A & (B | ~C) -> (A & B) | (A & ~C)
reveal_type(x) # revealed: A & B | A & ~C
reveal_type(x) # revealed: (A & B) | (A & ~C)
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
@@ -206,7 +206,7 @@ def _(x: A | B | C):
# ~A | (~A & C) | (~B & C) ->
# ~A | (C & ~B) ->
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
reveal_type(x) # revealed: (B & ~A) | (C & ~A) | (C & ~B)
```
## Boolean expression internal narrowing

View File

@@ -20,11 +20,9 @@ def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x == 1:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
reveal_type(x) # revealed: Literal[1]
elif x == 2:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(x) # revealed: Literal[2]
else:
reveal_type(x) # revealed: Literal[3]
```
@@ -38,14 +36,11 @@ def _(flag1: bool, flag2: bool):
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
elif x != 2:
# TODO should be `Literal[1]`
reveal_type(x) # revealed: Literal[1, 3]
reveal_type(x) # revealed: Literal[1]
elif x == 3:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
reveal_type(x) # revealed: Never
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(x) # revealed: Never
```
## Assignment expressions

View File

@@ -9,8 +9,7 @@ def _(flag: bool):
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
# TODO should be None
reveal_type(x) # revealed: None | Literal[1]
reveal_type(x) # revealed: None
```
## `!=` for other singleton types
@@ -22,8 +21,7 @@ def _(flag: bool):
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
# TODO should be Literal[False]
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: Literal[False]
```
## `x != y` where `y` is of literal type
@@ -47,8 +45,7 @@ def _(flag: bool):
if C != A:
reveal_type(C) # revealed: Literal[B]
else:
# TODO should be Literal[A]
reveal_type(C) # revealed: Literal[A, B]
reveal_type(C) # revealed: Literal[A]
```
## `x != y` where `y` has multiple single-valued options
@@ -61,8 +58,7 @@ def _(flag1: bool, flag2: bool):
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(x) # revealed: Literal[2]
```
## `!=` for non-single-valued types
@@ -101,6 +97,61 @@ def f() -> Literal[1, 2, 3]:
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]
reveal_type(x) # revealed: Literal[1]
```
## Union with `Any`
```py
from typing import Any
def _(x: Any | None, y: Any | None):
if x != 1:
reveal_type(x) # revealed: (Any & ~Literal[1]) | None
if y == 1:
reveal_type(y) # revealed: Any & ~None
```
## Booleans and integers
```py
from typing import Literal
def _(b: bool, i: Literal[1, 2]):
if b == 1:
reveal_type(b) # revealed: Literal[True]
else:
reveal_type(b) # revealed: Literal[False]
if b == 6:
reveal_type(b) # revealed: Never
else:
reveal_type(b) # revealed: bool
if b == 0:
reveal_type(b) # revealed: Literal[False]
else:
reveal_type(b) # revealed: Literal[True]
if i == True:
reveal_type(i) # revealed: Literal[1]
else:
reveal_type(i) # revealed: Literal[2]
```
## Narrowing `LiteralString` in union
```py
from typing_extensions import Literal, LiteralString, Any
def _(s: LiteralString | None, t: LiteralString | Any):
if s == "foo":
reveal_type(s) # revealed: Literal["foo"]
if s == 1:
reveal_type(s) # revealed: Never
if t == "foo":
# TODO could be `Literal["foo"] | Any`
reveal_type(t) # revealed: LiteralString | Any
```

View File

@@ -31,17 +31,14 @@ def _(flag1: bool, flag2: bool):
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x == 2:
# TODO should be `Literal[2]`
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(x) # revealed: Literal[2]
elif x == 3:
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: Never
elif x != 2:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 3]
reveal_type(x) # revealed: Literal[1]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
reveal_type(x) # revealed: Never
```

View File

@@ -82,19 +82,19 @@ class B: ...
def f(x: A | B):
if x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy)
if x and not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy)
else:
reveal_type(x) # revealed: A | B
if x or not x:
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy)
```
### Truthiness of Types
@@ -111,9 +111,9 @@ x = int if flag() else str
reveal_type(x) # revealed: Literal[int, str]
if x:
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
reveal_type(x) # revealed: (Literal[int] & ~AlwaysFalsy) | (Literal[str] & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
reveal_type(x) # revealed: (Literal[int] & ~AlwaysTruthy) | (Literal[str] & ~AlwaysTruthy)
```
## Determined Truthiness
@@ -176,12 +176,12 @@ if isinstance(x, str) and not isinstance(x, B):
z = x if flag() else y
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"]
if z:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"]
else:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""]
```
## Narrowing Multiple Variables
@@ -264,13 +264,13 @@ def _(
):
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
if ta:
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy
reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy)
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
if af:
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`"
if d:
# TODO: Should be `Unknown`
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
@@ -296,12 +296,12 @@ def _(x: Literal[0, 1]):
reveal_type(x and A()) # revealed: Literal[0] | A
def _(x: str):
reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A
reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A
def _(x: bool | str):
reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A
class Falsy:
def __bool__(self) -> Literal[False]:

View File

@@ -127,7 +127,7 @@ class B: ...
def _[T](x: A | B):
if type(x) is A[str]:
reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown]
reveal_type(x) # revealed: (A[int] & A[Unknown]) | (B & A[Unknown])
else:
reveal_type(x) # revealed: A[int] | B
```

View File

@@ -242,7 +242,7 @@ def f(
Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime:
```py
# TODO: should be `Literal[True]`
# Could also be `Literal[True]`, but `bool` is fine:
reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
```
@@ -304,10 +304,12 @@ reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool
## Calls to protocol classes
<!-- snapshot-diagnostics -->
Neither `Protocol`, nor any protocol class, can be directly instantiated:
```py
from typing import Protocol
from typing_extensions import Protocol, reveal_type
# error: [call-non-callable]
reveal_type(Protocol()) # revealed: Unknown
@@ -315,7 +317,7 @@ reveal_type(Protocol()) # revealed: Unknown
class MyProtocol(Protocol):
x: int
# error
# error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
reveal_type(MyProtocol()) # revealed: MyProtocol
```
@@ -363,25 +365,8 @@ class Foo(Protocol):
def method_member(self) -> bytes:
return b"foo"
# TODO: at runtime, `get_protocol_members` returns a `frozenset`,
# but for now we might pretend it returns a `tuple`, as we support heterogeneous `tuple` types
# but not yet generic `frozenset`s
#
# So this should either be
#
# `tuple[Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]`
#
# `frozenset[Literal["x", "y", "z", "method_member"]]`
reveal_type(get_protocol_members(Foo)) # revealed: @Todo(specialized non-generic class)
```
Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
```py
class NotAProtocol: ...
# TODO: should emit `[invalid-protocol]` error, should reveal `Unknown`
reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(specialized non-generic class)
# TODO: actually a frozenset (requires support for legacy generics)
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["method_member"], Literal["x"], Literal["y"], Literal["z"]]
```
Certain special attributes and methods are not considered protocol members at runtime, and should
@@ -399,8 +384,87 @@ class Lumberjack(Protocol):
def __init__(self, x: int) -> None:
self.x = x
# TODO: `tuple[Literal["x"]]` or `frozenset[Literal["x"]]`
reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class)
# TODO: actually a frozenset
reveal_type(get_protocol_members(Lumberjack)) # revealed: tuple[Literal["x"]]
```
A sub-protocol inherits and extends the members of its superclass protocol(s):
```py
class Bar(Protocol):
spam: str
class Baz(Bar, Protocol):
ham: memoryview
# TODO: actually a frozenset
reveal_type(get_protocol_members(Baz)) # revealed: tuple[Literal["ham"], Literal["spam"]]
class Baz2(Bar, Foo, Protocol): ...
# TODO: actually a frozenset
# revealed: tuple[Literal["method_member"], Literal["spam"], Literal["x"], Literal["y"], Literal["z"]]
reveal_type(get_protocol_members(Baz2))
```
## Protocol members in statically known branches
The list of protocol members does not include any members declared in branches that are statically
known to be unreachable:
```toml
[environment]
python-version = "3.9"
```
```py
import sys
from typing_extensions import Protocol, get_protocol_members
class Foo(Protocol):
if sys.version_info >= (3, 10):
a: int
b = 42
def c(self) -> None: ...
else:
d: int
e = 56
def f(self) -> None: ...
# TODO: actually a frozenset
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["d"], Literal["e"], Literal["f"]]
```
## Invalid calls to `get_protocol_members()`
<!-- snapshot-diagnostics -->
Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import Protocol, get_protocol_members
class NotAProtocol: ...
get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
class AlsoNotAProtocol(NotAProtocol, object): ...
get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
```
The original class object must be passed to the function; a specialised version of a generic version
does not suffice:
```py
class GenericProtocol[T](Protocol): ...
get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549)
```
## Subtyping of protocols with attribute members
@@ -409,6 +473,11 @@ In the following example, the protocol class `HasX` defines an interface such th
static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a
mutable `x` attribute of type `int`:
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Protocol
from knot_extensions import static_assert, is_assignable_to, is_subtype_of
@@ -548,9 +617,61 @@ def f(arg: HasXWithDefault):
reveal_type(type(arg).x) # revealed: int
```
Assignments in a class body of a protocol -- of any kind -- are not permitted by red-knot unless the
symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is
stricter validation of protocol members than many other type checkers currently apply (as of
2025/04/21).
The reason for this strict validation is that undeclared variables in the class body would lead to
an ambiguous interface being declared by the protocol.
```py
from typing_extensions import TypeAlias, get_protocol_members
class MyContext:
def __enter__(self) -> int:
return 42
def __exit__(self, *args) -> None: ...
class LotsOfBindings(Protocol):
a: int
a = 42 # this is fine, since `a` is declared in the class body
b: int = 56 # this is also fine, by the same principle
type c = str # this is very strange but I can't see a good reason to disallow it
d: TypeAlias = bytes # same here
class Nested: ... # also weird, but we should also probably allow it
class NestedProtocol(Protocol): ... # same here...
e = 72 # TODO: this should error with `[invalid-protocol]` (`e` is not declared)
f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared)
h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared)
for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared)
pass
with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared)
pass
match object():
case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared)
...
# TODO: actually a frozenset
# revealed: tuple[Literal["Nested"], Literal["NestedProtocol"], Literal["a"], Literal["b"], Literal["c"], Literal["d"], Literal["e"], Literal["f"], Literal["g"], Literal["h"], Literal["i"], Literal["j"], Literal["k"], Literal["l"]]
reveal_type(get_protocol_members(LotsOfBindings))
```
Attribute members are allowed to have assignments in methods on the protocol class, just like
non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that
are not declared in the class body -- are not allowed:
non-protocol classes. Unlike other classes, however, instance attributes that are not declared in
the class body are disallowed. This is mandated by [the spec][spec_protocol_members]:
> Additional attributes *only* defined in the body of a method by assignment via `self` are not
> allowed. The rationale for this is that the protocol class implementation is often not shared by
> subtypes, so the interface should not depend on the default implementation.
```py
class Foo(Protocol):
@@ -559,11 +680,33 @@ class Foo(Protocol):
def __init__(self) -> None:
self.x = 42 # fine
self.a = 56 # error
self.a = 56 # TODO: should emit diagnostic
self.b: int = 128 # TODO: should emit diagnostic
def non_init_method(self) -> None:
self.y = 64 # fine
self.b = 72 # error
self.c = 72 # TODO: should emit diagnostic
# Note: the list of members does not include `a`, `b` or `c`,
# as none of these attributes is declared in the class body.
#
# TODO: actually a frozenset
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["non_init_method"], Literal["x"], Literal["y"]]
```
If a member is declared in a superclass of a protocol class, it is fine for it to be assigned to in
the sub-protocol class without a redeclaration:
```py
class Super(Protocol):
x: int
class Sub(Super, Protocol):
x = 42 # no error here, since it's declared in the superclass
# TODO: actually frozensets
reveal_type(get_protocol_members(Super)) # revealed: tuple[Literal["x"]]
reveal_type(get_protocol_members(Sub)) # revealed: tuple[Literal["x"]]
```
If a protocol has 0 members, then all other types are assignable to it, and all fully static types
@@ -1077,23 +1220,25 @@ static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # er
## Narrowing of protocols
<!-- snapshot-diagnostics -->
By default, a protocol class cannot be used as the second argument to `isinstance()` or
`issubclass()`, and a type checker must emit an error on such calls. However, we still narrow the
type inside these branches (this matches the behaviour of other type checkers):
```py
from typing import Protocol
from typing_extensions import Protocol, reveal_type
class HasX(Protocol):
x: int
def f(arg: object, arg2: type):
if isinstance(arg, HasX): # error
if isinstance(arg, HasX): # error: [invalid-argument-type]
reveal_type(arg) # revealed: HasX
else:
reveal_type(arg) # revealed: ~HasX
if issubclass(arg2, HasX): # error
if issubclass(arg2, HasX): # error: [invalid-argument-type]
reveal_type(arg2) # revealed: type[HasX]
else:
reveal_type(arg2) # revealed: type & ~type[HasX]
@@ -1128,10 +1273,10 @@ class OnlyMethodMembers(Protocol):
def method(self) -> None: ...
def f(arg1: type, arg2: type):
if issubclass(arg1, OnlyMethodMembers): # error
reveal_type(arg1) # revealed: type[OnlyMethodMembers]
if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members)
reveal_type(arg1) # revealed: type[RuntimeCheckableHasX]
else:
reveal_type(arg1) # revealed: type & ~type[OnlyMethodMembers]
reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX]
if issubclass(arg2, OnlyMethodMembers): # no error!
reveal_type(arg2) # revealed: type[OnlyMethodMembers]
@@ -1139,6 +1284,140 @@ def f(arg1: type, arg2: type):
reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
```
## Truthiness of protocol instance
An instance of a protocol type generally has ambiguous truthiness:
```py
from typing import Protocol
class Foo(Protocol):
x: int
def f(foo: Foo):
reveal_type(bool(foo)) # revealed: bool
```
But this is not the case if the protocol has a `__bool__` method member that returns `Literal[True]`
or `Literal[False]`:
```py
from typing import Literal
class Truthy(Protocol):
def __bool__(self) -> Literal[True]: ...
class FalsyFoo(Foo, Protocol):
def __bool__(self) -> Literal[False]: ...
class FalsyFooSubclass(FalsyFoo, Protocol):
y: str
def g(a: Truthy, b: FalsyFoo, c: FalsyFooSubclass):
reveal_type(bool(a)) # revealed: Literal[True]
reveal_type(bool(b)) # revealed: Literal[False]
reveal_type(bool(c)) # revealed: Literal[False]
```
It is not sufficient for a protocol to have a callable `__bool__` instance member that returns
`Literal[True]` for it to be considered always truthy. Dunder methods are looked up on the class
rather than the instance. If a protocol `X` has an instance-attribute `__bool__` member, it is
unknowable whether that attribute can be accessed on the type of an object that satisfies `X`'s
interface:
```py
from typing import Callable
class InstanceAttrBool(Protocol):
__bool__: Callable[[], Literal[True]]
def h(obj: InstanceAttrBool):
reveal_type(bool(obj)) # revealed: bool
```
## Fully static protocols; gradual protocols
A protocol is only fully static if all of its members are fully static:
```py
from typing import Protocol, Any
from knot_extensions import is_fully_static, static_assert
class FullyStatic(Protocol):
x: int
class NotFullyStatic(Protocol):
x: Any
static_assert(is_fully_static(FullyStatic))
# TODO: should pass
static_assert(not is_fully_static(NotFullyStatic)) # error: [static-assert-error]
```
Non-fully-static protocols do not participate in subtyping, only assignability:
```py
from knot_extensions import is_subtype_of, is_assignable_to
class NominalWithX:
x: int = 42
# TODO: these should pass
static_assert(is_assignable_to(NominalWithX, FullyStatic)) # error: [static-assert-error]
static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) # error: [static-assert-error]
static_assert(is_subtype_of(NominalWithX, FullyStatic)) # error: [static-assert-error]
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic))
```
Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to
the nominal type `object` (as described above):
```py
class Empty(Protocol): ...
static_assert(is_fully_static(Empty))
```
A method member is only considered fully static if all its parameter annotations and its return
annotation are fully static:
```py
class FullyStaticMethodMember(Protocol):
def method(self, x: int) -> str: ...
class DynamicParameter(Protocol):
def method(self, x: Any) -> str: ...
class DynamicReturn(Protocol):
def method(self, x: int) -> Any: ...
static_assert(is_fully_static(FullyStaticMethodMember))
# TODO: these should pass
static_assert(not is_fully_static(DynamicParameter)) # error: [static-assert-error]
static_assert(not is_fully_static(DynamicReturn)) # error: [static-assert-error]
```
The [typing spec][spec_protocol_members] states:
> If any parameters of a protocol method are not annotated, then their types are assumed to be `Any`
Thus, a partially unannotated method member can also not be considered to be fully static:
```py
class NoParameterAnnotation(Protocol):
def method(self, x) -> str: ...
class NoReturnAnnotation(Protocol):
def method(self, x: int): ...
# TODO: these should pass
static_assert(not is_fully_static(NoParameterAnnotation)) # error: [static-assert-error]
static_assert(not is_fully_static(NoReturnAnnotation)) # error: [static-assert-error]
```
## `typing.SupportsIndex` and `typing.Sized`
`typing.SupportsIndex` is already somewhat supported through some special-casing in red-knot.
@@ -1167,10 +1446,10 @@ def _(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
Add tests for:
- Assignments without declarations in protocol class bodies. And various weird ways of creating
attributes in a class body or instance method. [Example mypy tests][mypy_weird_protocols].
- More tests for protocols inside `type[]`. [Spec reference][protocols_inside_type_spec].
- Protocols with instance-method members
- Protocols with instance-method members, including:
- Protocols with methods that have parameters or the return type unannotated
- Protocols with methods that have parameters or the return type annotated with `Any`
- Protocols with `@classmethod` and `@staticmethod`
- Assignability of non-instance types to protocols with instance-method members (e.g. a
class-literal type can be a subtype of `Sized` if its metaclass has a `__len__` method)
@@ -1184,16 +1463,13 @@ Add tests for:
- Protocols with instance attributes annotated with `Callable` (can a nominal type with a method
satisfy that protocol, and if so in what cases?)
- Protocols decorated with `@final`
- Protocols with attribute members annotated with `Any`
- Protocols with methods that have parameters or the return type unannotated
- Protocols with methods that have parameters or the return type annotated with `Any`
- Equivalence and subtyping between `Callable` types and protocols that define `__call__`
[mypy_protocol_docs]: https://mypy.readthedocs.io/en/stable/protocols.html#protocols-and-structural-subtyping
[mypy_protocol_tests]: https://github.com/python/mypy/blob/master/test-data/unit/check-protocols.test
[mypy_weird_protocols]: https://github.com/python/mypy/blob/a3ce6d5307e99a1b6c181eaa7c5cf134c53b7d8b/test-data/unit/check-protocols.test#L2131-L2132
[protocol conformance tests]: https://github.com/python/typing/tree/main/conformance/tests
[protocols_inside_type_spec]: https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols
[recursive_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#recursive-protocols
[self_types_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#self-types-in-protocols
[spec_protocol_members]: https://typing.python.org/en/latest/spec/protocol.html#protocol-members
[typing_spec_protocols]: https://typing.python.org/en/latest/spec/protocol.html

View File

@@ -404,7 +404,7 @@ x = int
class C:
var: ClassVar[x]
reveal_type(C.var) # revealed: Unknown | str
reveal_type(C.var) # revealed: str
x = str
```

View File

@@ -0,0 +1,177 @@
# `global` references
## Implicit global in function
A name reference to a never-defined symbol in a function is implicitly a global lookup.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Explicit global in function
```py
x = 1
def f():
global x
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Unassignable type in function
```py
x: int = 1
def f():
y: int = 1
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
y = ""
global x
# TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
x = ""
```
## Nested intervening scope
A `global` statement causes lookup to skip any bindings in intervening scopes:
```py
x: int = 1
def outer():
x: str = ""
def inner():
global x
# TODO: revealed: int
reveal_type(x) # revealed: str
```
## Narrowing
An assignment following a `global` statement should narrow the type in the local scope after the
assignment.
```py
x: int | None
def f():
global x
x = 1
reveal_type(x) # revealed: Literal[1]
```
## `nonlocal` and `global`
A binding cannot be both `nonlocal` and `global`. This should emit a semantic syntax error. CPython
marks the `nonlocal` line, while `mypy`, `pyright`, and `ruff` (`PLE0115`) mark the `global` line.
```py
x = 1
def f():
x = 1
def g() -> None:
nonlocal x
global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global"
x = None
```
## Global declaration after `global` statement
```py
def f():
global x
# TODO this should also not be an error
y = x # error: [unresolved-reference] "Name `x` used when not defined"
x = 1 # No error.
x = 2
```
## Semantic syntax errors
Using a name prior to its `global` declaration in the same scope is a syntax error.
```py
x = 1
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
global x
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
global x, y
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
global x
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
global x
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
# still an error in module scope
x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
```

View File

@@ -43,14 +43,3 @@ def f():
def h():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Implicit global in function
A name reference to a never-defined symbol in a function is implicitly a global lookup.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
```

View File

@@ -5,7 +5,7 @@
```py
class C: ...
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
C = 1 # error: "Implicit shadowing of class `C`"
```
## Explicit

View File

@@ -15,7 +15,7 @@ def f(x: str):
```py
def f(): ...
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
f = 1 # error: "Implicit shadowing of function `f`"
```
## Explicit shadowing

View File

@@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:11:1
error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
--> src/mdtest_snippet.py:11:1
|
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
11 | instance.attr = 1 # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
| ^^^^^^^^^^^^^
|
```

View File

@@ -29,12 +29,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:12:1
error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
--> src/mdtest_snippet.py:12:1
|
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
12 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
| ^^^^^^^^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:6:1
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> src/mdtest_snippet.py:6:1
|
4 | instance = C()
5 | instance.attr = 1 # fine
6 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^^^^^^^^
7 |
8 | C.attr = 1 # fine
|
@@ -40,12 +40,12 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:9:1
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> src/mdtest_snippet.py:9:1
|
8 | C.attr = 1 # fine
9 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
warning: lint:possibly-unbound-attribute
--> /src/mdtest_snippet.py:6:5
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]` is possibly unbound
--> src/mdtest_snippet.py:6:5
|
4 | attr: int = 0
5 |
6 | C.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^ Attribute `attr` on type `Literal[C]` is possibly unbound
| ^^^^^^
7 |
8 | instance = C()
|
@@ -40,12 +40,12 @@ warning: lint:possibly-unbound-attribute
```
```
warning: lint:possibly-unbound-attribute
--> /src/mdtest_snippet.py:9:5
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `C` is possibly unbound
--> src/mdtest_snippet.py:9:5
|
8 | instance = C()
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^^^^^^^^ Attribute `attr` on type `C` is possibly unbound
| ^^^^^^^^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:7:1
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> src/mdtest_snippet.py:7:1
|
5 | instance = C()
6 | instance.attr = 1 # fine
7 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^^^^^^^^
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
|
@@ -40,13 +40,13 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-attribute-access
--> /src/mdtest_snippet.py:9:1
error: lint:invalid-attribute-access: Cannot assign to instance attribute `attr` from the class object `Literal[C]`
--> src/mdtest_snippet.py:9:1
|
7 | instance.attr = "wrong" # error: [invalid-assignment]
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]`
| ^^^^^^
|
```

View File

@@ -37,12 +37,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:11:5
error: lint:invalid-assignment: Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
--> src/mdtest_snippet.py:11:5
|
10 | # TODO: The error message here could be improved to explain why the assignment fails.
11 | C1.attr = 1 # error: [invalid-assignment]
| ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
| ^^^^^^^
12 |
13 | class C2:
|

View File

@@ -23,13 +23,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:unresolved-attribute
--> /src/mdtest_snippet.py:3:1
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `Literal[C]`.
--> src/mdtest_snippet.py:3:1
|
1 | class C: ...
2 |
3 | C.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`.
| ^^^^^^^^^^^^^^
4 |
5 | instance = C()
|
@@ -37,12 +37,12 @@ error: lint:unresolved-attribute
```
```
error: lint:unresolved-attribute
--> /src/mdtest_snippet.py:6:1
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `C`.
--> src/mdtest_snippet.py:6:1
|
5 | instance = C()
6 | instance.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`.
| ^^^^^^^^^^^^^^^^^^^^^
|
```

View File

@@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
--> /src/mdtest_snippet.py:7:1
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> src/mdtest_snippet.py:7:1
|
6 | C.attr = 1 # fine
7 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^
8 |
9 | instance = C()
|
@@ -40,12 +40,12 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-attribute-access
--> /src/mdtest_snippet.py:10:1
error: lint:invalid-attribute-access: Cannot assign to ClassVar `attr` from an instance of type `C`
--> src/mdtest_snippet.py:10:1
|
9 | instance = C()
10 | instance.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C`
| ^^^^^^^^^^^^^
|
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:1:8
error: lint:unresolved-import: Cannot resolve import `zqzqzqzqzqzqzq`
--> src/mdtest_snippet.py:1:8
|
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`
| ^^^^^^^^^^^^^^
|
```

View File

@@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Diagnostics
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:2:8
error: lint:unresolved-import: Cannot resolve import `a.foo`
--> src/mdtest_snippet.py:2:8
|
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
| ^^^^^ Cannot resolve import `a.foo`
| ^^^^^
3 |
4 | # Topmost component unresolvable:
|
@@ -40,12 +40,12 @@ error: lint:unresolved-import
```
```
error: lint:unresolved-import
--> /src/mdtest_snippet.py:5:8
error: lint:unresolved-import: Cannot resolve import `b.foo`
--> src/mdtest_snippet.py:5:8
|
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
| ^^^^^ Cannot resolve import `b.foo`
| ^^^^^
|
```

View File

@@ -28,20 +28,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:10:10
error: lint:not-iterable: Object of type `Iterable` is not iterable
--> src/mdtest_snippet.py:10:10
|
9 | # error: [not-iterable]
10 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^
11 | reveal_type(x) # revealed: int
|
info: It has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol
info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:11:5
--> src/mdtest_snippet.py:11:5
|
9 | # error: [not-iterable]
10 | for x in Iterable():

View File

@@ -20,13 +20,14 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:2:10
error: lint:not-iterable: Object of type `Literal[123]` is not iterable
--> src/mdtest_snippet.py:2:10
|
1 | nonsense = 123
2 | for x in nonsense: # error: [not-iterable]
| ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
| ^^^^^^^^
3 | pass
|
info: It doesn't have an `__iter__` method or a `__getitem__` method
```

View File

@@ -24,14 +24,15 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:6:10
error: lint:not-iterable: Object of type `NotIterable` is not iterable
--> src/mdtest_snippet.py:6:10
|
4 | __iter__: None = None
5 |
6 | for x in NotIterable(): # error: [not-iterable]
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
| ^^^^^^^^^^^^^
7 | pass
|
info: Its `__iter__` attribute has type `None`, which is not callable
```

View File

@@ -25,20 +25,21 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:7:10
error: lint:not-iterable: Object of type `Bad` is not iterable
--> src/mdtest_snippet.py:7:10
|
6 | # error: [not-iterable]
7 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
| ^^^^^
8 | reveal_type(x) # revealed: Unknown
|
info: It has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:8:5
--> src/mdtest_snippet.py:8:5
|
6 | # error: [not-iterable]
7 | for x in Bad():

View File

@@ -46,21 +46,23 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:22:14
error: lint:not-iterable: Object of type `Iterable1` may not be iterable
--> src/mdtest_snippet.py:22:14
|
21 | # error: [not-iterable]
22 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
| ^^^^^^^^^^^
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
|
info: It has no `__iter__` method and its `__getitem__` attribute is invalid
info: `__getitem__` has type `CustomCallable`, which is not callable
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:24:9
--> src/mdtest_snippet.py:24:9
|
22 | for x in Iterable1():
23 | # TODO... `int` might be ideal here?
@@ -73,21 +75,23 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:27:14
error: lint:not-iterable: Object of type `Iterable2` may not be iterable
--> src/mdtest_snippet.py:27:14
|
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 Iterable2.__getitem__(key: int) -> int) | None`) may not be callable
| ^^^^^^^^^^^
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
|
info: It has no `__iter__` method and its `__getitem__` attribute is invalid
info: `__getitem__` has type `(bound method Iterable2.__getitem__(key: int) -> int) | None`, which is not callable
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:29:9
--> src/mdtest_snippet.py:29:9
|
27 | for y in Iterable2():
28 | # TODO... `int` might be ideal here?

View File

@@ -43,21 +43,23 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:20:14
error: lint:not-iterable: Object of type `Iterable1` may not be iterable
--> src/mdtest_snippet.py:20:14
|
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 Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
| ^^^^^^^^^^^
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
|
info: It has no `__iter__` method and its `__getitem__` attribute is invalid
info: `__getitem__` has type `(bound method Iterable1.__getitem__(item: int) -> str) | None`, which is not callable
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:22:9
--> src/mdtest_snippet.py:22:9
|
20 | for x in Iterable1():
21 | # TODO: `str` might be better
@@ -70,20 +72,22 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:25:14
error: lint:not-iterable: Object of type `Iterable2` may not be iterable
--> src/mdtest_snippet.py:25:14
|
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 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: 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
info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:26:9
--> src/mdtest_snippet.py:26:9
|
24 | # error: [not-iterable]
25 | for y in Iterable2():

View File

@@ -47,20 +47,23 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:17:14
error: lint:not-iterable: Object of type `Iterable1` may not be iterable
--> src/mdtest_snippet.py:17:14
|
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 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: Its `__iter__` method may have an invalid signature
info: Type of `__iter__` is `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`
info: Expected signature for `__iter__` is `def __iter__(self): ...`
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:18:9
--> src/mdtest_snippet.py:18:9
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
@@ -73,21 +76,22 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:28:14
error: lint:not-iterable: Object of type `Iterable2` may not be iterable
--> src/mdtest_snippet.py:28:14
|
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 Iterable2.__iter__() -> Iterator) | None`) may not be callable
| ^^^^^^^^^^^
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
|
info: Its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:30:9
--> src/mdtest_snippet.py:30:9
|
28 | for x in Iterable2():
29 | # TODO: `int` would probably be better here:

View File

@@ -51,20 +51,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:28:14
error: lint:not-iterable: Object of type `Iterable1` may not be iterable
--> src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
| ^^^^^^^^^^^
29 | reveal_type(x) # revealed: int | str
|
info: Its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method
info: Expected signature for `__next__` is `def __next__(self): ...`)
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:29:9
--> src/mdtest_snippet.py:29:9
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
@@ -77,21 +79,22 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:32:14
error: lint:not-iterable: Object of type `Iterable2` may not be iterable
--> src/mdtest_snippet.py:32:14
|
31 | # error: [not-iterable]
32 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
| ^^^^^^^^^^^
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
|
info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:34:9
--> src/mdtest_snippet.py:34:9
|
32 | for y in Iterable2():
33 | # TODO: `int` would probably be better here:

View File

@@ -36,20 +36,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:18:14
error: lint:not-iterable: Object of type `Iterable` may not be iterable
--> src/mdtest_snippet.py:18:14
|
17 | # error: [not-iterable]
18 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^
19 | reveal_type(x) # revealed: int | bytes
|
info: It may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol
info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:19:9
--> src/mdtest_snippet.py:19:9
|
17 | # error: [not-iterable]
18 | for x in Iterable():

View File

@@ -54,21 +54,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:31:14
error: lint:not-iterable: Object of type `Iterable1` may not be iterable
--> src/mdtest_snippet.py:31:14
|
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 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
|
info: 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
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:33:9
--> src/mdtest_snippet.py:33:9
|
31 | for x in Iterable1():
32 | # TODO: `bytes | str` might be better
@@ -81,20 +82,22 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:36:14
error: lint:not-iterable: Object of type `Iterable2` may not be iterable
--> src/mdtest_snippet.py:36:14
|
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 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: 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
info: `__getitem__` must be at least as permissive as `def __getitem__(self, key: int): ...` to satisfy the old-style iteration protocol
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:37:9
--> src/mdtest_snippet.py:37:9
|
35 | # error: [not-iterable]
36 | for y in Iterable2():

View File

@@ -35,20 +35,21 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:17:14
error: lint:not-iterable: Object of type `Iterable` may not be iterable
--> src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
| ^^^^^^^^^^
18 | reveal_type(x) # revealed: int | bytes
|
info: It may not have an `__iter__` method or a `__getitem__` method
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:18:9
--> src/mdtest_snippet.py:18:9
|
16 | # error: [not-iterable]
17 | for x in Iterable():

View File

@@ -36,21 +36,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:18:14
error: lint:not-iterable: Object of type `Test | Test2` may not be iterable
--> src/mdtest_snippet.py:18:14
|
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
19 | reveal_type(x) # revealed: int
|
info: Its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:19:9
--> src/mdtest_snippet.py:19:9
|
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():

View File

@@ -31,21 +31,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:13:14
error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterable
--> src/mdtest_snippet.py:13:14
|
11 | def _(flag: bool):
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
| ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
| ^^^^^^^^^^^^^^^^^^^^^^
14 | reveal_type(x) # revealed: int
|
info: It may not have an `__iter__` method and it doesn't have a `__getitem__` method
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:14:9
--> src/mdtest_snippet.py:14:9
|
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:

View File

@@ -33,32 +33,33 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:11:14
error: lint:not-iterable: Object of type `NotIterable` is not iterable
--> src/mdtest_snippet.py:11:14
|
10 | # error: [not-iterable]
11 | for x in NotIterable():
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
| ^^^^^^^^^^^^^
12 | pass
|
info: Its `__iter__` attribute has type `int | None`, which is not callable
```
```
warning: lint:possibly-unresolved-reference
--> /src/mdtest_snippet.py:16:17
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> src/mdtest_snippet.py:16:17
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
| ^ Name `x` used when possibly not defined
| ^
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:16:5
--> src/mdtest_snippet.py:16:5
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]

View File

@@ -26,20 +26,21 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:8:10
error: lint:not-iterable: Object of type `Bad` is not iterable
--> src/mdtest_snippet.py:8:10
|
7 | # error: [not-iterable]
8 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
| ^^^^^
9 | reveal_type(x) # revealed: Unknown
|
info: Its `__iter__` method returns an object of type `int`, which has no `__next__` method
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:9:5
--> src/mdtest_snippet.py:9:5
|
7 | # error: [not-iterable]
8 | for x in Bad():

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