Compare commits

...

10 Commits

Author SHA1 Message Date
Alex Waygood
68bdf9fa88 [ty] Make special cases for subscript inference exhaustive 2026-01-14 11:47:19 +00:00
Charlie Marsh
b5814b91c1 [ty] Add diagnostics to validate TypeIs and TypeGuard definitions (#22300)
## Summary

Closes https://github.com/astral-sh/ty/issues/2267.
2026-01-13 20:24:05 -05:00
Charlie Marsh
ea46426157 [ty] Apply narrowing to walrus targets (#22369)
## Summary

Closes https://github.com/astral-sh/ty/issues/2300.
2026-01-14 00:56:47 +00:00
Alex Waygood
ddd2fc7a90 [ty] Use "typeguard constraints" for two kinds of tuple narrowing (#22348)
## Summary

Since we've already filtered the union in these locations, it seems like
needless overhead to then intersect the previous union with the filtered
union. We know what that intersection will simplify to: it will simplify
to the filtered union. So rather than using a regular intersection-based
constraint, we can use a "typeguard constraint", which will just
directly replace the previous type with the new type instead of creating
an intersection.

## Test Plan

- Existing tests all pass
- The primer report should be clean
2026-01-13 23:37:09 +00:00
Will Duke
4ebf10cf1b [ty] Add a conformance script to compare ty diagnostics with expected errors (#22231) 2026-01-13 22:19:41 +00:00
Charlie Marsh
9a676bbeb7 [ty] Add diagnostic to catch generic enums (#22482)
## Summary

Closes https://github.com/astral-sh/ty/issues/2416.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2026-01-13 16:55:46 -05:00
Bhuminjay Soni
d9028a098b [isort] Insert imports in alphabetical order (I002) (#22493)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
This PR fixes #20811 , current approach reverses the order in `BtreeSet`
however as pointed in
https://github.com/astral-sh/ruff/issues/20811#issuecomment-3398958832
here we cannot use I`IndexSet` to preserve config order since Settings
derives `CacheKey` which isn't implemented for `IndexSet`, another
approach to preserve the original order might be to use `Vec` however
lookup time complexity might get affected as a result.

<!-- How was it tested? -->
I have tested it locally its working as expected ,
<img width="2200" height="1071" alt="image"
src="https://github.com/user-attachments/assets/7d97b488-1552-4a42-9d90-92acf55ec493"
/>

---------

Signed-off-by: Bhuminjay <bhuminjaysoni@gmail.com>
2026-01-13 21:21:18 +00:00
Alex Waygood
56077ee9a9 [ty] Fix @Todo type for starred expressions (#22503) 2026-01-13 21:09:29 +00:00
Alex Waygood
20c01d2553 [ty] Use the top materialization of classes for if type(x) is y narrowing (#22553) 2026-01-13 20:53:52 +00:00
Harutaka Kawamura
c98ea1bc24 [flake8-pytest-style] Add check parameter example to PT017 docs (#22546)
## Summary

- Adds an alternative example to the PT017 (`pytest-assert-in-except`)
rule documentation showing pytest's `check` parameter for validating
exceptions, available since pytest 8.4.0

Closes #22529

## Test plan

Documentation-only change. Verified with `uvx prek run -a`.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:45:53 -05:00
33 changed files with 1662 additions and 238 deletions

View File

@@ -106,6 +106,16 @@ impl Violation for PytestCompositeAssertion {
/// assert exc_info.value.args
/// ```
///
/// Or, for pytest 8.4.0 and later:
/// ```python
/// import pytest
///
///
/// def test_foo():
/// with pytest.raises(ZeroDivisionError, check=lambda e: e.args):
/// 1 / 0
/// ```
///
/// ## References
/// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises)
#[derive(ViolationMetadata)]

View File

@@ -140,7 +140,7 @@ pub(crate) fn add_required_imports(
source_type: PySourceType,
context: &LintContext,
) {
for required_import in &settings.isort.required_imports {
for required_import in settings.isort.required_imports.iter().rev() {
add_required_import(
required_import,
parsed,

View File

@@ -1,14 +1,6 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
I002 [*] Missing required import: `from __future__ import annotations`
--> docstring.py:1:1
help: Insert required import: `from __future__ import annotations`
1 | """Hello, world!"""
2 + from __future__ import annotations
3 |
4 | x = 1
I002 [*] Missing required import: `from __future__ import generator_stop`
--> docstring.py:1:1
help: Insert required import: `from __future__ import generator_stop`
@@ -16,3 +8,11 @@ help: Insert required import: `from __future__ import generator_stop`
2 + from __future__ import generator_stop
3 |
4 | x = 1
I002 [*] Missing required import: `from __future__ import annotations`
--> docstring.py:1:1
help: Insert required import: `from __future__ import annotations`
1 | """Hello, world!"""
2 + from __future__ import annotations
3 |
4 | x = 1

View File

@@ -1,15 +1,6 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
I002 [*] Missing required import: `from __future__ import annotations`
--> multiple_strings.py:1:1
help: Insert required import: `from __future__ import annotations`
1 | """This is a docstring."""
2 + from __future__ import annotations
3 | "This is not a docstring."
4 | "This is also not a docstring."
5 |
I002 [*] Missing required import: `from __future__ import generator_stop`
--> multiple_strings.py:1:1
help: Insert required import: `from __future__ import generator_stop`
@@ -17,4 +8,13 @@ help: Insert required import: `from __future__ import generator_stop`
2 + from __future__ import generator_stop
3 | "This is not a docstring."
4 | "This is also not a docstring."
5 |
I002 [*] Missing required import: `from __future__ import annotations`
--> multiple_strings.py:1:1
help: Insert required import: `from __future__ import annotations`
1 | """This is a docstring."""
2 + from __future__ import annotations
3 | "This is not a docstring."
4 | "This is also not a docstring."
5 |

View File

@@ -398,17 +398,17 @@ mod tests {
1 + from pipes import Template
2 + from shlex import quote
I002 [*] Missing required import: `from __future__ import generator_stop`
--> <filename>:1:1
help: Insert required import: `from __future__ import generator_stop`
1 + from __future__ import generator_stop
2 | from pipes import quote, Template
I002 [*] Missing required import: `from collections import Sequence`
--> <filename>:1:1
help: Insert required import: `from collections import Sequence`
1 + from collections import Sequence
2 | from pipes import quote, Template
I002 [*] Missing required import: `from __future__ import generator_stop`
--> <filename>:1:1
help: Insert required import: `from __future__ import generator_stop`
1 + from __future__ import generator_stop
2 | from pipes import quote, Template
");
}

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

@@ -8,7 +8,7 @@
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L542" target="_blank">View source</a>
</small>
@@ -80,7 +80,7 @@ def test(): -> "int":
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L140" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L141" target="_blank">View source</a>
</small>
@@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.7">0.0.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-top-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L158" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L159" target="_blank">View source</a>
</small>
@@ -135,7 +135,7 @@ def f(x: object):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L209" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L210" target="_blank">View source</a>
</small>
@@ -167,7 +167,7 @@ f(int) # error
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L235" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L236" target="_blank">View source</a>
</small>
@@ -198,7 +198,7 @@ a = 1
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L261" target="_blank">View source</a>
</small>
@@ -230,7 +230,7 @@ class C(A, B): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L287" target="_blank">View source</a>
</small>
@@ -262,7 +262,7 @@ class B(A): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L312" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L313" target="_blank">View source</a>
</small>
@@ -290,7 +290,7 @@ type B = A
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L356" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L357" target="_blank">View source</a>
</small>
@@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L334" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L335" target="_blank">View source</a>
</small>
@@ -346,7 +346,7 @@ false positives it can produce.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L377" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L378" target="_blank">View source</a>
</small>
@@ -373,7 +373,7 @@ class B(A, A): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L398" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L399" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L624" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L625" target="_blank">View source</a>
</small>
@@ -559,7 +559,7 @@ class C(A, B): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L648" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L649" target="_blank">View source</a>
</small>
@@ -585,7 +585,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L431" target="_blank">View source</a>
</small>
@@ -674,7 +674,7 @@ an atypical memory layout.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L703" target="_blank">View source</a>
</small>
@@ -701,7 +701,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L742" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L743" target="_blank">View source</a>
</small>
@@ -729,7 +729,7 @@ a: int = ''
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2079" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2122" target="_blank">View source</a>
</small>
@@ -763,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L764" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L765" target="_blank">View source</a>
</small>
@@ -799,7 +799,7 @@ asyncio.run(main())
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L794" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L795" target="_blank">View source</a>
</small>
@@ -823,7 +823,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L879" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L880" target="_blank">View source</a>
</small>
@@ -850,7 +850,7 @@ with 1:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L900" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L901" target="_blank">View source</a>
</small>
@@ -879,7 +879,7 @@ a: str
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L923" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L924" target="_blank">View source</a>
</small>
@@ -923,7 +923,7 @@ except ZeroDivisionError:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1749" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1792" target="_blank">View source</a>
</small>
@@ -965,7 +965,7 @@ class D(A):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.35">0.0.1-alpha.35</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-frozen-dataclass-subclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2330" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2373" target="_blank">View source</a>
</small>
@@ -1009,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1002" target="_blank">View source</a>
</small>
@@ -1041,6 +1041,55 @@ class D(Generic[U, T]): ...
- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)
## `invalid-generic-enum`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.12">0.0.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-enum" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L960" target="_blank">View source</a>
</small>
**What it does**
Checks for enum classes that are also generic.
**Why is this bad?**
Enum classes cannot be generic. Python does not support generic enums:
attempting to create one will either result in an immediate `TypeError`
at runtime, or will create a class that cannot be specialized in the way
that a normal generic class can.
**Examples**
```python
from enum import Enum
from typing import Generic, TypeVar
T = TypeVar("T")
# error: enum class cannot be generic (class creation fails with `TypeError`)
class E[T](Enum):
A = 1
# error: enum class cannot be generic (class creation fails with `TypeError`)
class F(Enum, Generic[T]):
A = 1
# error: enum class cannot be generic -- the class creation does not immediately fail...
class G(Generic[T], Enum):
A = 1
# ...but this raises `KeyError`:
x: G[int]
```
**References**
- [Python documentation: Enum](https://docs.python.org/3/library/enum.html)
## `invalid-ignore-comment`
<small>
@@ -1077,7 +1126,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L669" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L670" target="_blank">View source</a>
</small>
@@ -1116,7 +1165,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L990" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1033" target="_blank">View source</a>
</small>
@@ -1151,7 +1200,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1087" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1130" target="_blank">View source</a>
</small>
@@ -1185,7 +1234,7 @@ class B(metaclass=f): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2232" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2275" target="_blank">View source</a>
</small>
@@ -1292,7 +1341,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L576" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L577" target="_blank">View source</a>
</small>
@@ -1346,7 +1395,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1106" target="_blank">View source</a>
</small>
@@ -1376,7 +1425,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1114" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1157" target="_blank">View source</a>
</small>
@@ -1426,7 +1475,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1213" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1256" target="_blank">View source</a>
</small>
@@ -1452,7 +1501,7 @@ def f(a: int = ''): ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1018" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061" target="_blank">View source</a>
</small>
@@ -1483,7 +1532,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L512" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L513" target="_blank">View source</a>
</small>
@@ -1517,7 +1566,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1233" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1276" target="_blank">View source</a>
</small>
@@ -1566,7 +1615,7 @@ def g():
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L723" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L724" target="_blank">View source</a>
</small>
@@ -1591,7 +1640,7 @@ def func() -> int:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1276" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1319" target="_blank">View source</a>
</small>
@@ -1687,7 +1736,7 @@ class C: ...
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.10">0.0.10</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-total-ordering" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2368" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2411" target="_blank">View source</a>
</small>
@@ -1733,7 +1782,7 @@ class MyClass:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1042" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1085" target="_blank">View source</a>
</small>
@@ -1760,7 +1809,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1508" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1551" target="_blank">View source</a>
</small>
@@ -1807,7 +1856,7 @@ Bar[int] # error: too few arguments
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1315" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1358" target="_blank">View source</a>
</small>
@@ -1837,7 +1886,7 @@ TYPE_CHECKING = ''
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1339" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1382" target="_blank">View source</a>
</small>
@@ -1867,7 +1916,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1391" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1434" target="_blank">View source</a>
</small>
@@ -1901,7 +1950,7 @@ f(10) # Error
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1363" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1406" target="_blank">View source</a>
</small>
@@ -1935,7 +1984,7 @@ class C:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1419" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1462" target="_blank">View source</a>
</small>
@@ -1970,7 +2019,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.9">0.0.9</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-typed-dict-statement" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2207" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2250" target="_blank">View source</a>
</small>
@@ -2001,7 +2050,7 @@ class Foo(TypedDict):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1448" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1491" target="_blank">View source</a>
</small>
@@ -2026,7 +2075,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2180" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2223" target="_blank">View source</a>
</small>
@@ -2059,7 +2108,7 @@ alice["age"] # KeyError
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1467" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1510" target="_blank">View source</a>
</small>
@@ -2088,7 +2137,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1549" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1592" target="_blank">View source</a>
</small>
@@ -2114,7 +2163,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1490" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1533" target="_blank">View source</a>
</small>
@@ -2138,7 +2187,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1722" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1765" target="_blank">View source</a>
</small>
@@ -2171,7 +2220,7 @@ class B(A):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1600" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1643" target="_blank">View source</a>
</small>
@@ -2198,7 +2247,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1933" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1976" target="_blank">View source</a>
</small>
@@ -2225,7 +2274,7 @@ f(x=1) # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1621" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1664" target="_blank">View source</a>
</small>
@@ -2253,7 +2302,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L184" target="_blank">View source</a>
</small>
@@ -2285,7 +2334,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1643" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1686" target="_blank">View source</a>
</small>
@@ -2322,7 +2371,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1673" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1716" target="_blank">View source</a>
</small>
@@ -2386,7 +2435,7 @@ def test(): -> "int":
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2107" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2150" target="_blank">View source</a>
</small>
@@ -2413,7 +2462,7 @@ cast(int, f()) # Redundant
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2055" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2098" target="_blank">View source</a>
</small>
@@ -2443,7 +2492,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1699" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1742" target="_blank">View source</a>
</small>
@@ -2472,7 +2521,7 @@ class B(A): ... # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1867" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1910" target="_blank">View source</a>
</small>
@@ -2506,7 +2555,7 @@ class F(NamedTuple):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1807" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1850" target="_blank">View source</a>
</small>
@@ -2533,7 +2582,7 @@ f("foo") # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1785" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1828" target="_blank">View source</a>
</small>
@@ -2561,7 +2610,7 @@ def _(x: int):
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1828" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1871" target="_blank">View source</a>
</small>
@@ -2607,7 +2656,7 @@ class A:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1894" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1937" target="_blank">View source</a>
</small>
@@ -2631,7 +2680,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1912" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1955" target="_blank">View source</a>
</small>
@@ -2658,7 +2707,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1954" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1997" target="_blank">View source</a>
</small>
@@ -2686,7 +2735,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2128" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2171" target="_blank">View source</a>
</small>
@@ -2744,7 +2793,7 @@ def g():
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1976" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2019" target="_blank">View source</a>
</small>
@@ -2769,7 +2818,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1995" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2038" target="_blank">View source</a>
</small>
@@ -2794,7 +2843,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L812" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L813" target="_blank">View source</a>
</small>
@@ -2833,7 +2882,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1569" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1612" target="_blank">View source</a>
</small>
@@ -2870,7 +2919,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-dynamic-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L845" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L846" target="_blank">View source</a>
</small>
@@ -2911,7 +2960,7 @@ def factory(base: type[Base]) -> type:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2014" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2057" target="_blank">View source</a>
</small>
@@ -2975,7 +3024,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments.
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1157" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1200" target="_blank">View source</a>
</small>
@@ -3038,7 +3087,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2036" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2079" target="_blank">View source</a>
</small>

View File

@@ -42,6 +42,12 @@ def f[T](x: T, cond: bool) -> T | list[T]:
return x if cond else [x]
l5: int | list[int] = f(1, True)
a: list[int] = [1, 2, *(3, 4, 5)]
reveal_type(a) # revealed: list[int]
b: list[list[int]] = [[1], [2], *([3], [4])]
reveal_type(b) # revealed: list[list[int]]
```
`typed_dict.py`:

View File

@@ -834,7 +834,7 @@ class Base: ...
bases_tuple = (Base,)
Cls1 = type("Cls1", (*bases_tuple,), {})
reveal_type(Cls1) # revealed: <class 'Cls1'>
reveal_mro(Cls1) # revealed: (<class 'Cls1'>, @Todo(StarredExpression), <class 'object'>)
reveal_mro(Cls1) # revealed: (<class 'Cls1'>, <class 'Base'>, <class 'object'>)
# Unpacking a dict for the namespace - the dict contents are not tracked anyway
namespace = {"attr": 1}

View File

@@ -25,3 +25,22 @@ B = bytes
reveal_mro(C) # revealed: (<class 'C'>, <class 'int'>, <class 'G[bytes]'>, typing.Generic, <class 'object'>)
```
## Starred bases
These are currently not supported, but ideally we would support them in some limited situations.
```py
from ty_extensions import reveal_mro
class A: ...
class B: ...
class C: ...
bases = (A, B, C)
class Foo(*bases): ...
# revealed: (<class 'Foo'>, @Todo(StarredExpression), <class 'object'>)
reveal_mro(Foo)
```

View File

@@ -1016,6 +1016,108 @@ class Color(Enum):
reveal_type(Color.RED != Color.RED) # revealed: bool
```
## Generic enums are invalid
Enum classes cannot be generic. Python does not support generic enums, and attempting to create one
will result in a `TypeError` at runtime.
### PEP 695 syntax
Using PEP 695 type parameters on an enum is invalid:
```toml
[environment]
python-version = "3.12"
```
```py
from enum import Enum
# error: [invalid-generic-enum] "Enum class `E` cannot be generic"
class E[T](Enum):
A = 1
B = 2
```
### Legacy `Generic` base class
Inheriting from both `Enum` and `Generic[T]` is also invalid:
```py
from enum import Enum
from typing import Generic, TypeVar
T = TypeVar("T")
# error: [invalid-generic-enum] "Enum class `F` cannot be generic"
class F(Enum, Generic[T]):
A = 1
B = 2
```
### Swapped order (`Generic` first)
The order of bases doesn't matter; it's still invalid:
```py
from enum import Enum
from typing import Generic, TypeVar
T = TypeVar("T")
# error: [invalid-generic-enum] "Enum class `G` cannot be generic"
class G(Generic[T], Enum):
A = 1
B = 2
```
### Enum subclasses
Subclasses of enum base classes also cannot be generic:
```toml
[environment]
python-version = "3.12"
```
```py
from enum import Enum, IntEnum
from typing import Generic, TypeVar
T = TypeVar("T")
# error: [invalid-generic-enum] "Enum class `MyIntEnum` cannot be generic"
class MyIntEnum[T](IntEnum):
A = 1
# error: [invalid-generic-enum] "Enum class `MyFlagEnum` cannot be generic"
class MyFlagEnum(IntEnum, Generic[T]):
A = 1
```
### Custom enum base class
Even with custom enum subclasses that don't have members, they cannot be made generic:
```toml
[environment]
python-version = "3.12"
```
```py
from enum import Enum
from typing import Generic, TypeVar
T = TypeVar("T")
class MyEnumBase(Enum):
def some_method(self) -> None: ...
# error: [invalid-generic-enum] "Enum class `MyEnum` cannot be generic"
class MyEnum[T](MyEnumBase):
A = 1
```
## References
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>

View File

@@ -43,13 +43,11 @@ reveal_type(len((1,))) # revealed: Literal[1]
reveal_type(len((1, 2))) # revealed: Literal[2]
reveal_type(len(tuple())) # revealed: Literal[0]
# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[],))) # revealed: Literal[1]
reveal_type(len((*[],))) # revealed: Literal[0]
# fmt: off
# TODO: Handle star unpacks; Should be: Literal[1]
reveal_type(len( # revealed: Literal[2]
reveal_type(len( # revealed: Literal[1]
(
*[],
1,
@@ -58,11 +56,8 @@ reveal_type(len( # revealed: Literal[2]
# fmt: on
# TODO: Handle star unpacks; Should be: Literal[2]
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[], *{}))) # revealed: Literal[2]
reveal_type(len((*[], 1, 2))) # revealed: Literal[2]
reveal_type(len((*[], *{}))) # revealed: Literal[0]
```
Tuple subclasses:

View File

@@ -797,7 +797,7 @@ class B(A):
pass
class C[T]:
def check(x: object) -> TypeIs[T]:
def check(self, x: object) -> TypeIs[T]:
# this is a bad check, but we only care about it type-checking
return False
@@ -835,7 +835,7 @@ class B(A):
pass
class C[T]:
def check(x: object) -> TypeGuard[T]:
def check(self, x: object) -> TypeGuard[T]:
# this is a bad check, but we only care about it type-checking
return False

View File

@@ -69,6 +69,30 @@ def call_with_args(y: object, a: int, b: str) -> object:
return None
```
## Narrowing with named expressions (walrus operator)
When `callable()` is used with a named expression, the target of the named expression should be
narrowed.
```py
from typing import Any
class Foo:
func: Any | None
def f(foo: Foo):
first = getattr(foo, "func", None)
if callable(first):
reveal_type(first) # revealed: Any & Top[(...) -> object]
else:
reveal_type(first) # revealed: (Any & ~Top[(...) -> object]) | None
if callable(second := getattr(foo, "func", None)):
reveal_type(second) # revealed: Any & Top[(...) -> object]
else:
reveal_type(second) # revealed: (Any & ~Top[(...) -> object]) | None
```
## Assignability of narrowed callables
A narrowed callable `Top[Callable[..., object]]` should be assignable to `Callable[..., Any]`. This

View File

@@ -580,3 +580,19 @@ def test(a: Any, items: list[T]) -> None:
if isinstance(v, dict):
cast(T, v) # no panic
```
## Narrowing with named expressions (walrus operator)
When `isinstance()` is used with a named expression, the target of the named expression should be
narrowed.
```py
def get_value() -> int | str:
return 1
def f():
if isinstance(x := get_value(), int):
reveal_type(x) # revealed: int
else:
reveal_type(x) # revealed: str
```

View File

@@ -347,3 +347,19 @@ def _(x: LiteralString):
else:
reveal_type(x) # revealed: LiteralString & ~Literal[""]
```
## Narrowing with named expressions (walrus operator)
When a truthiness check is used with a named expression, the target of the named expression should
be narrowed.
```py
def get_value() -> str | None:
return "hello"
def f():
if x := get_value():
reveal_type(x) # revealed: str & ~AlwaysFalsy
else:
reveal_type(x) # revealed: (str & ~AlwaysTruthy) | None
```

View File

@@ -70,6 +70,47 @@ def _(x: A | B, y: A | C):
reveal_type(y) # revealed: A
```
## The top materialization is used for generic classes
```py
# list is invariant
def f(x: list[int] | None):
if type(x) is list:
reveal_type(x) # revealed: list[int]
else:
reveal_type(x) # revealed: list[int] | None
if type(x) is not list:
reveal_type(x) # revealed: list[int] | None
else:
reveal_type(x) # revealed: list[int]
# frozenset is covariant
def g(x: frozenset[bytes] | None):
if type(x) is frozenset:
reveal_type(x) # revealed: frozenset[bytes]
else:
reveal_type(x) # revealed: frozenset[bytes] | None
if type(x) is not frozenset:
reveal_type(x) # revealed: frozenset[bytes] | None
else:
reveal_type(x) # revealed: frozenset[bytes]
def h(x: object):
if type(x) is list:
reveal_type(x) # revealed: Top[list[Unknown]]
elif type(x) is frozenset:
reveal_type(x) # revealed: frozenset[object]
else:
reveal_type(x) # revealed: object
if type(x) is not list and type(x) is not frozenset:
reveal_type(x) # revealed: object
else:
reveal_type(x) # revealed: Top[list[Unknown]] | frozenset[object]
```
## No narrowing for `type(x) is C[int]`
At runtime, `type(x)` will never return a generic alias object (only ever a class-literal object),
@@ -234,8 +275,7 @@ An early version of <https://github.com/astral-sh/ruff/pull/19920> caused us to
```py
def _(val):
if type(val) is tuple:
# TODO: better would be `Unknown & tuple[object, ...]`
reveal_type(val) # revealed: Unknown & tuple[Unknown, ...]
reveal_type(val) # revealed: Unknown & tuple[object, ...]
```
## Limitations

View File

@@ -14,8 +14,8 @@ def _(
b: TypeIs[str | int],
c: TypeGuard[bool],
d: TypeIs[tuple[TypeOf[bytes]]],
e: TypeGuard, # error: [invalid-type-form]
f: TypeIs, # error: [invalid-type-form]
e: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression"
f: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
):
reveal_type(a) # revealed: TypeGuard[str]
reveal_type(b) # revealed: TypeIs[str | int]
@@ -46,12 +46,23 @@ A user-defined type guard must accept at least one positional argument (in addit
for non-static methods).
```pyi
from typing import Any, TypeVar
from typing_extensions import TypeGuard, TypeIs
# TODO: error: [invalid-type-guard-definition]
T = TypeVar("T")
# Multiple parameters are allowed
def is_str_list(val: list[object], allow_empty: bool) -> TypeGuard[list[str]]: ...
def is_set_of(val: set[Any], type: type[T]) -> TypeGuard[set[T]]: ...
def is_two_element_tuple(val: tuple[object, ...], a: str, b: str) -> TypeIs[tuple[str, str]]: ...
# error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
def _() -> TypeGuard[str]: ...
# TODO: error: [invalid-type-guard-definition]
# error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
def _(*args) -> TypeGuard[str]: ...
# error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
def _(**kwargs) -> TypeIs[str]: ...
class _:
@@ -63,14 +74,14 @@ class _:
def _(a) -> TypeIs[str]: ...
# errors
def _(self) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(self, /, *, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(self) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
def _(self, /, *, a) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
@classmethod
def _(cls) -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(cls) -> TypeIs[str]: ... # error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
@classmethod
def _() -> TypeIs[str]: ... # TODO: error: [invalid-type-guard-definition]
def _() -> TypeIs[str]: ... # error: [invalid-type-guard-definition] "`TypeIs` function must have a parameter to narrow"
@staticmethod
def _(*, a) -> TypeGuard[str]: ... # TODO: error: [invalid-type-guard-definition]
def _(*, a) -> TypeGuard[str]: ... # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
```
For `TypeIs` functions, the narrowed type must be assignable to the declared type of that parameter,
@@ -86,10 +97,10 @@ def _(a: tuple[object]) -> TypeIs[tuple[str]]: ...
def _(a: str | Any) -> TypeIs[str]: ...
def _(a) -> TypeIs[str]: ...
# TODO: error: [invalid-type-guard-definition]
# error: [invalid-type-guard-definition] "Narrowed type `str` is not assignable to the declared parameter type `int`"
def _(a: int) -> TypeIs[str]: ...
# TODO: error: [invalid-type-guard-definition]
# error: [invalid-type-guard-definition] "Narrowed type `int` is not assignable to the declared parameter type `bool | str`"
def _(a: bool | str) -> TypeIs[int]: ...
```
@@ -107,12 +118,14 @@ class C:
@classmethod
def g(cls, x: object) -> TypeGuard[int]:
return True
# TODO: this could error at definition time
def h(self) -> TypeGuard[str]:
def h(
self,
) -> TypeGuard[str]: # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
return True
# TODO: this could error at definition time
@classmethod
def j(cls) -> TypeGuard[int]:
def j(cls) -> TypeGuard[int]: # error: [invalid-type-guard-definition] "`TypeGuard` function must have a parameter to narrow"
return True
def _(x: object):
@@ -221,7 +234,7 @@ def g(a: object) -> TypeIs[int]:
return True
def _(d: Any):
if f(): # error: [missing-argument]
if f(): # error: [missing-argument] "No argument provided for required parameter `a` of function `f`"
...
if g(*d):
@@ -230,7 +243,7 @@ def _(d: Any):
if f("foo"): # TODO: error: [invalid-type-guard-call]
...
if g(a=d): # error: [invalid-type-guard-call]
if g(a=d): # error: [invalid-type-guard-call] "Type guard call does not have a target"
...
```
@@ -499,3 +512,32 @@ def _(x: object):
if f(x) and (g(x) or h(x)):
reveal_type(x) # revealed: B | (A & C)
```
## Narrowing with named expressions (walrus operator)
When a type guard is used with a named expression, the target of the named expression should be
narrowed.
```py
from typing_extensions import TypeGuard, TypeIs
def is_str(x: object) -> TypeIs[str]:
return isinstance(x, str)
def guard_str(x: object) -> TypeGuard[str]:
return isinstance(x, str)
def get_value() -> int | str:
return 1
def f():
if is_str(x := get_value()):
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: int
if guard_str(y := get_value()):
reveal_type(y) # revealed: str
else:
reveal_type(y) # revealed: int | str
```

View File

@@ -0,0 +1,43 @@
# Subscripts involving type aliases
Aliases are expanded during analysis of subscripts.
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import TypeAlias, Literal
ImplicitTuple = tuple[str, int, int]
PEP613Tuple: TypeAlias = tuple[str, int, int]
type PEP695Tuple = tuple[str, int, int]
ImplicitZero = Literal[0]
PEP613Zero: TypeAlias = Literal[0]
type PEP695Zero = Literal[0]
def f(
implicit_tuple: ImplicitTuple,
pep_613_tuple: PEP613Tuple,
pep_695_tuple: PEP695Tuple,
implicit_zero: ImplicitZero,
pep_613_zero: PEP613Zero,
pep_695_zero: PEP695Zero,
):
reveal_type(implicit_tuple[:2]) # revealed: tuple[str, int]
reveal_type(implicit_tuple[implicit_zero]) # revealed: str
reveal_type(implicit_tuple[pep_613_zero]) # revealed: str
reveal_type(implicit_tuple[pep_695_zero]) # revealed: str
reveal_type(pep_613_tuple[:2]) # revealed: tuple[str, int]
reveal_type(pep_613_tuple[implicit_zero]) # revealed: str
reveal_type(pep_613_tuple[pep_613_zero]) # revealed: str
reveal_type(pep_613_tuple[pep_695_zero]) # revealed: str
reveal_type(pep_695_tuple[:2]) # revealed: tuple[str, int]
reveal_type(pep_695_tuple[implicit_zero]) # revealed: str
reveal_type(pep_695_tuple[pep_613_zero]) # revealed: str
reveal_type(pep_695_tuple[pep_695_zero]) # revealed: str
```

View File

@@ -106,5 +106,5 @@ class Bar:
def f(x: Foo):
if isinstance(x, Bar):
# TODO: should be `int`
reveal_type(x["whatever"]) # revealed: @Todo(Subscript expressions on intersections)
reveal_type(x["whatever"]) # revealed: @Todo(Subscript expressions with intersections)
```

View File

@@ -80,6 +80,17 @@ def _(m: int, n: int, s2: str):
reveal_type(substring2) # revealed: str
```
## LiteralString
```py
from typing_extensions import LiteralString
def f(x: LiteralString):
reveal_type(x[0]) # revealed: LiteralString
reveal_type(x[True]) # revealed: LiteralString
reveal_type(x[1:42]) # revealed: LiteralString
```
## Unsupported slice types
```py

View File

@@ -430,5 +430,5 @@ class Bar: ...
def test4(val: Intersection[tuple[Foo], tuple[Bar]]):
# TODO: should be `Foo & Bar`
reveal_type(val[0]) # revealed: @Todo(Subscript expressions on intersections)
reveal_type(val[0]) # revealed: @Todo(Subscript expressions with intersections)
```

View File

@@ -0,0 +1,89 @@
# Subscripts involving type variables
## TypeVar bound/constrained to a tuple/int-literal/bool-literal
The upper bounds of type variables are considered when analysing subscripts.
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import TypeAlias, Literal
ImplicitTuple = tuple[str, int, int]
PEP613Tuple: TypeAlias = tuple[str, int, int]
type PEP695Tuple = tuple[str, int, int]
ImplicitZero = Literal[0]
PEP613Zero: TypeAlias = Literal[0]
type PEP695Zero = Literal[0]
# fmt: off
def f[
BoundedTupleT: tuple[str, int, bytes],
ConstrainedTupleT: (tuple[str, int, bytes], tuple[int, bytes, str]),
BoundedZeroT: Literal[0],
ConstrainedIntLiteralT: (Literal[0], Literal[1])
](
tuple_1: BoundedTupleT,
tuple_2: ConstrainedTupleT,
zero: BoundedZeroT,
some_integer: ConstrainedIntLiteralT,
):
# TODO: would ideally be `tuple[str, int]`
reveal_type(tuple_1[:2]) # revealed: tuple[str | int | bytes, ...]
reveal_type(tuple_1[zero]) # revealed: str
# TODO: ideally this might be `str | int`,
# but it's hard to do that without introducing false positives elsewhere
reveal_type(tuple_1[some_integer]) # revealed: str | int | bytes
# TODO: would ideally be `tuple[str, int] | tuple[int, bytes]`
reveal_type(tuple_2[:2]) # revealed: tuple[str | int | bytes, ...]
reveal_type(tuple_2[zero]) # revealed: str | int
reveal_type(tuple_2[some_integer]) # revealed: str | int | bytes
# fmt: on
```
## TypeVars
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Protocol
class SupportsLessThan(Protocol):
def __lt__(self, other, /) -> bool: ...
def f[K: SupportsLessThan](dictionary: dict[K, int], key: K):
reveal_type(dictionary[key]) # revealed: int
```
## ParamSpecs
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Callable
def decorator[**P, T](func: Callable[P, T]) -> Callable[P, T]:
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
if len(args) > 0:
# error: [invalid-assignment]
args = args[1:]
# `func` requires the full `ParamSpec` passed into `decorator`,
# but here the first argument is skipped, so we should possibly emit an error here:
return func(*args, **kwargs)
return inner
```

View File

@@ -534,4 +534,34 @@ x: list[Literal[1, 2, 3]] = list((1, 2, 3))
reveal_type(x) # revealed: list[Literal[1, 2, 3]]
```
## Tuples with starred elements
```py
from typing import Literal, Sequence
x = (1, *range(3), 3)
reveal_type(x) # revealed: tuple[Literal[1], *tuple[int, ...], Literal[3]]
y = 1, 2
reveal_type(("foo", *y)) # revealed: tuple[Literal["foo"], Literal[1], Literal[2]]
aa: tuple[list[int], ...] = ([42], *{[56], [78]}, [100])
reveal_type(aa) # revealed: tuple[list[int], list[int], list[int], list[int]]
bb: tuple[list[Literal[42, 56]], ...] = ([42], *{[56, 42], [42]}, [42, 42, 56])
reveal_type(bb) # revealed: tuple[list[Literal[42, 56]], list[Literal[42, 56]], list[Literal[42, 56]], list[Literal[42, 56]]]
reveal_type((*[],)) # revealed: tuple[()]
reveal_type((42, *[], 56, *[])) # revealed: tuple[Literal[42], Literal[56]]
tup: Sequence[str] = (*{"foo": 42, "bar": 56},)
# TODO: `tuple[str, str]` would be better, given the type annotation
reveal_type(tup) # revealed: tuple[Unknown | str, Unknown | str]
def f(x: list[int]):
reveal_type((42, 56, *x, 97)) # revealed: tuple[Literal[42], Literal[56], *tuple[int, ...], Literal[97]]
```
[not a singleton type]: https://discuss.python.org/t/should-we-specify-in-the-language-reference-that-the-empty-tuple-is-a-singleton/67957

View File

@@ -38,6 +38,12 @@ impl PlaceExpr {
pub(crate) fn try_from_expr<'e>(expr: impl Into<ast::ExprRef<'e>>) -> Option<Self> {
let expr = expr.into();
// For named expressions (walrus operator), extract the target.
let expr = match expr {
ast::ExprRef::Named(named) => named.target.as_ref().into(),
_ => expr,
};
if let ast::ExprRef::Name(name) = expr {
return Some(PlaceExpr::Symbol(Symbol::new(name.id.clone())));
}

View File

@@ -3256,12 +3256,16 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
// This is one of the few places where we want to check if there's _any_ specialization
// where assignability holds; normally we want to check that assignability holds for
// _all_ specializations.
//
// TODO: Soon we will go further, and build the actual specializations from the
// constraint set that we get from this assignability check, instead of inferring and
// building them in an earlier separate step.
if argument_type
.when_assignable_to(self.db, expected_ty, self.inferable_typevars)
.is_never_satisfied(self.db)
//
// TODO: handle starred annotations, e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...]]`
if !parameter.has_starred_annotation()
&& argument_type
.when_assignable_to(self.db, expected_ty, self.inferable_typevars)
.is_never_satisfied(self.db)
{
let positional = matches!(argument, Argument::Positional | Argument::Synthetic)
&& !parameter.is_variadic();

View File

@@ -45,10 +45,10 @@ use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion
use crate::types::{
ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType,
CallableTypeKind, CallableTypes, DATACLASS_FLAGS, DataclassFlags, DataclassParams,
DeprecatedInstance, FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownInstanceType,
ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType,
TypeAliasType, TypeContext, TypeMapping, TypedDictParams, UnionBuilder, VarianceInferable,
binding_type, declaration_type, determine_upper_bound,
DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, IntersectionBuilder,
KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor,
PropertyInstanceType, TypeAliasType, TypeContext, TypeMapping, TypedDictParams, UnionBuilder,
VarianceInferable, binding_type, declaration_type, determine_upper_bound,
};
use crate::{
Db, FxIndexMap, FxIndexSet, FxOrderSet, Program,
@@ -664,14 +664,6 @@ impl<'db> ClassLiteral<'db> {
}
}
/// Returns an unknown specialization for this class.
pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
match self {
Self::Static(class) => class.unknown_specialization(db),
Self::Dynamic(_) => ClassType::NonGeneric(self),
}
}
/// Returns the definition of this class.
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
match self {
@@ -2379,6 +2371,11 @@ impl<'db> StaticClassLiteral<'db> {
let module = parsed_module(db, self.file(db)).load(db);
let class_stmt = self.node(db, &module);
if class_stmt.bases().iter().any(ast::Expr::is_starred_expr) {
return Box::new([Type::Dynamic(DynamicType::TodoStarredExpression)]);
}
let class_definition =
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);

View File

@@ -73,6 +73,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_CONTEXT_MANAGER);
registry.register_lint(&INVALID_DECLARATION);
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_GENERIC_ENUM);
registry.register_lint(&INVALID_GENERIC_CLASS);
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
registry.register_lint(&INVALID_PARAMSPEC);
@@ -956,6 +957,48 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for enum classes that are also generic.
///
/// ## Why is this bad?
/// Enum classes cannot be generic. Python does not support generic enums:
/// attempting to create one will either result in an immediate `TypeError`
/// at runtime, or will create a class that cannot be specialized in the way
/// that a normal generic class can.
///
/// ## Examples
/// ```python
/// from enum import Enum
/// from typing import Generic, TypeVar
///
/// T = TypeVar("T")
///
/// # error: enum class cannot be generic (class creation fails with `TypeError`)
/// class E[T](Enum):
/// A = 1
///
/// # error: enum class cannot be generic (class creation fails with `TypeError`)
/// class F(Enum, Generic[T]):
/// A = 1
///
/// # error: enum class cannot be generic -- the class creation does not immediately fail...
/// class G(Generic[T], Enum):
/// A = 1
///
/// # ...but this raises `KeyError`:
/// x: G[int]
/// ```
///
/// ## References
/// - [Python documentation: Enum](https://docs.python.org/3/library/enum.html)
pub(crate) static INVALID_GENERIC_ENUM = {
summary: "detects generic enum classes",
status: LintStatus::stable("0.0.12"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for the creation of invalid generic classes

View File

@@ -65,15 +65,15 @@ use crate::types::diagnostic::{
CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE,
DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT,
INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS,
INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE,
INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases,
NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL,
UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
hint_if_stdlib_attribute_exists_on_other_versions,
INVALID_GENERIC_ENUM, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS,
INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT,
INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM,
INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_CONSTRAINTS,
INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR,
USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict,
@@ -92,6 +92,7 @@ use crate::types::diagnostic::{
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
report_unsupported_binary_operation, report_unsupported_comparison,
};
use crate::types::enums::is_enum_class_by_inheritance;
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
is_implicit_classmethod, is_implicit_staticmethod,
@@ -105,7 +106,7 @@ use crate::types::instance::SliceLiteral;
use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind};
use crate::types::newtype::NewType;
use crate::types::subclass_of::SubclassOfInner;
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleSpecBuilder, TupleType};
use crate::types::typed_dict::{
TypedDictAssignmentKind, validate_typed_dict_constructor, validate_typed_dict_dict_literal,
validate_typed_dict_key_assignment,
@@ -586,6 +587,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if self.db().should_check_file(self.file()) {
self.check_static_class_definitions();
self.check_overloaded_functions(node);
self.check_type_guard_definitions();
}
}
@@ -636,10 +638,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
continue;
}
// (2) Check that the class is not an enum and generic
if is_enum_class_by_inheritance(self.db(), class)
&& class.generic_context(self.db()).is_some()
{
if let Some(builder) = self.context.report_lint(&INVALID_GENERIC_ENUM, class_node) {
builder.into_diagnostic(format_args!(
"Enum class `{}` cannot be generic",
class.name(self.db())
));
}
}
let is_named_tuple =
CodeGeneratorKind::NamedTuple.matches(self.db(), class.into(), None);
// (2) If it's a `NamedTuple` class, check that no field without a default value
// (3) If it's a `NamedTuple` class, check that no field without a default value
// appears after a field with a default value.
if is_named_tuple {
let mut field_with_default_encountered = None;
@@ -680,7 +694,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut disjoint_bases = IncompatibleBases::default();
// (3) Iterate through the class's explicit bases to check for various possible errors:
// (4) Iterate through the class's explicit bases to check for various possible errors:
// - Check for inheritance from plain `Generic`,
// - Check for inheritance from a `@final` classes
// - If the class is a protocol class: check for inheritance from a non-protocol class
@@ -794,7 +808,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// (4) Check that the class's MRO is resolvable
// (5) Check that the class's MRO is resolvable
match class.try_mro(self.db(), None) {
Err(mro_error) => match mro_error.reason() {
StaticMroErrorKind::DuplicateBases(duplicates) => {
@@ -865,7 +879,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// (5) Check that @total_ordering has a valid ordering method in the MRO
// (6) Check that @total_ordering has a valid ordering method in the MRO
if class.total_ordering(self.db()) && !class.has_ordering_method_in_mro(self.db(), None)
{
// Find the @total_ordering decorator to report the diagnostic at its location
@@ -884,7 +898,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// (6) Check that the class's metaclass can be determined without error.
// (7) Check that the class's metaclass can be determined without error.
if let Err(metaclass_error) = class.try_metaclass(self.db()) {
match metaclass_error.reason() {
MetaclassErrorKind::Cycle => {
@@ -960,7 +974,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// (7) Check that the class arguments matches the arguments of the
// (8) Check that the class arguments matches the arguments of the
// base class `__init_subclass__` method.
if let Some(args) = class_node.arguments.as_deref() {
let call_args: CallArguments = args
@@ -1000,7 +1014,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// (8) If the class is generic, verify that its generic context does not violate any of
// (9) If the class is generic, verify that its generic context does not violate any of
// the typevar scoping rules.
if let (Some(legacy), Some(inherited)) = (
class.legacy_generic_context(self.db()),
@@ -1079,7 +1093,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// (9) Check that a dataclass does not have more than one `KW_ONLY`.
// (10) Check that a dataclass does not have more than one `KW_ONLY`.
if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) =
CodeGeneratorKind::from_class(self.db(), class.into(), None)
{
@@ -1114,7 +1128,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
// (10) Check for violations of the Liskov Substitution Principle,
// (11) Check for violations of the Liskov Substitution Principle,
// and for violations of other rules relating to invalid overrides of some sort.
overrides::check_class(&self.context, class);
@@ -1122,7 +1136,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
protocol.validate_members(&self.context);
}
// (11) If it's a `TypedDict` class, check that it doesn't include any invalid
// (12) If it's a `TypedDict` class, check that it doesn't include any invalid
// statements: https://typing.python.org/en/latest/spec/typeddict.html#class-based-syntax
//
// The body of the class definition defines the items of the `TypedDict` type. It
@@ -1439,6 +1453,85 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
/// Check that all type guard function definitions have at least one positional parameter
/// (in addition to `self`/`cls` for methods), and for `TypeIs`, that the narrowed type is
/// assignable to the declared type of that parameter.
fn check_type_guard_definitions(&mut self) {
for (definition, ty) in self.declarations.iter() {
// Only check actual function definitions, not imports.
let DefinitionKind::Function(function_ref) = definition.kind(self.db()) else {
continue;
};
let Some(function) = ty.inner_type().as_function_literal() else {
continue;
};
for overload in function.iter_overloads_and_implementation(self.db()) {
let signature = overload.signature(self.db());
let return_ty = signature.return_ty;
// Check if this is a `TypeIs` or `TypeGuard` return type.
let (type_guard_form_name, narrowed_type) = match return_ty {
Type::TypeIs(type_is) => ("TypeIs", Some(type_is.return_type(self.db()))),
Type::TypeGuard(_) => ("TypeGuard", None),
_ => continue,
};
let function_node = function_ref.node(self.module());
// The return type annotation must exist since we matched `TypeIs`/`TypeGuard`.
let Some(returns_expr) = function_node.returns.as_deref() else {
continue;
};
// Check if this is a non-static method (first parameter is implicit `self`/`cls`).
let is_method = self
.index
.class_definition_of_method(
overload.body_scope(self.db()).file_scope_id(self.db()),
)
.is_some();
let has_implicit_receiver = is_method && !overload.is_staticmethod(self.db());
// Find the first positional parameter to narrow (skip implicit `self`/`cls`).
let positional_params: Vec<_> = signature.parameters().positional().collect();
let first_narrowed_param_index = usize::from(has_implicit_receiver);
let first_narrowed_param = positional_params.get(first_narrowed_param_index);
let Some(first_narrowed_param) = first_narrowed_param else {
if let Some(builder) = self
.context
.report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr)
{
builder.into_diagnostic(format_args!(
"`{type_guard_form_name}` function must have a parameter to narrow"
));
}
continue;
};
// For `TypeIs`, check that the narrowed type is assignable to the parameter type.
if let Some(narrowed_ty) = narrowed_type {
let param_ty = first_narrowed_param.annotated_type();
if !narrowed_ty.is_assignable_to(self.db(), param_ty) {
if let Some(builder) = self
.context
.report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr)
{
builder.into_diagnostic(format_args!(
"Narrowed type `{narrowed}` is not assignable \
to the declared parameter type `{param}`",
narrowed = narrowed_ty.display(self.db()),
param = param_ty.display(self.db())
));
}
}
}
}
}
}
fn infer_region_definition(&mut self, definition: Definition<'db>) {
match definition.kind(self.db()) {
DefinitionKind::Function(function) => {
@@ -8154,7 +8247,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::Expr::If(if_expression) => self.infer_if_expression(if_expression, tcx),
ast::Expr::Lambda(lambda_expression) => self.infer_lambda_expression(lambda_expression),
ast::Expr::Call(call_expression) => self.infer_call_expression(call_expression, tcx),
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
ast::Expr::Starred(starred) => self.infer_starred_expression(starred, tcx),
ast::Expr::Yield(yield_expression) => self.infer_yield_expression(yield_expression),
ast::Expr::YieldFrom(yield_from) => self.infer_yield_from_expression(yield_from),
ast::Expr::Await(await_expression) => self.infer_await_expression(await_expression),
@@ -8399,7 +8492,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
parenthesized: _,
} = tuple;
// Remove any union elements of that are unrelated to the tuple type.
// Remove any union elements of the annotation that are unrelated to the tuple type.
let tcx = tcx.map(|annotation| {
let inferable = KnownClass::Tuple
.try_to_class_literal(self.db())
@@ -8413,16 +8506,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
)
});
let mut is_homogeneous_tuple_annotation = false;
let annotated_tuple = tcx
.known_specialization(self.db(), KnownClass::Tuple)
.and_then(|specialization| {
specialization
let spec = specialization
.tuple(self.db())
.expect("the specialization of `KnownClass::Tuple` must have a tuple spec")
.resize(self.db(), TupleLength::Fixed(elts.len()))
.ok()
.expect("the specialization of `KnownClass::Tuple` must have a tuple spec");
if let Tuple::Variable(tuple) = spec
&& tuple.prefix_elements().is_empty()
&& tuple.suffix_elements().is_empty()
{
is_homogeneous_tuple_annotation = true;
}
spec.resize(self.db(), TupleLength::Fixed(elts.len())).ok()
});
// TODO: this is a simplification for now.
//
// It might be possible to use the type context where the annotation is not a pure-homogeneous
// tuple and the actual tuple has starred elements in it. It seems complex to reason about,
// though, and unlikely to come up much.
let can_use_type_context =
is_homogeneous_tuple_annotation || elts.iter().all(|elt| !elt.is_starred_expr());
let mut annotated_elt_tys = annotated_tuple
.as_ref()
.map(Tuple::all_elements)
@@ -8431,12 +8541,59 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.copied();
let db = self.db();
let element_types = elts.iter().map(|element| {
let annotated_elt_ty = annotated_elt_tys.next();
self.infer_expression(element, TypeContext::new(annotated_elt_ty))
});
Type::heterogeneous_tuple(db, element_types)
let mut infer_element = |elt: &ast::Expr| {
let ctx = if can_use_type_context {
let annotated_elt_ty = annotated_elt_tys.by_ref().next();
let expected = if elt.is_starred_expr() {
let expected_element = annotated_elt_ty.unwrap_or_else(Type::object);
Some(KnownClass::Iterable.to_specialized_instance(db, &[expected_element]))
} else {
annotated_elt_ty
};
TypeContext::new(expected)
} else {
TypeContext::default()
};
self.infer_expression(elt, ctx)
};
let mut builder = TupleSpecBuilder::with_capacity(elts.len());
for element in elts {
if let ast::Expr::Starred(starred) = element {
let element_type = infer_element(element);
// Fine to use `iterate` rather than `try_iterate` here:
// errors from iterating over something not iterable will have been
// emitted in the `infer_element` call above.
let mut spec = element_type.iterate(db).into_owned();
let known_length = match &*starred.value {
ast::Expr::List(ast::ExprList { elts, .. })
| ast::Expr::Set(ast::ExprSet { elts, .. }) => elts
.iter()
.all(|elt| !elt.is_starred_expr())
.then_some(elts.len()),
ast::Expr::Dict(ast::ExprDict { items, .. }) => items
.iter()
.all(|item| item.key.is_some())
.then_some(items.len()),
_ => None,
};
if let Some(known_length) = known_length {
spec = spec
.resize(db, TupleLength::Fixed(known_length))
.unwrap_or(spec);
}
builder = builder.concat(db, &spec);
} else {
builder.push(infer_element(element));
}
}
Type::tuple(TupleType::new(db, &builder.build()))
}
fn infer_list_expression(&mut self, list: &ast::ExprList, tcx: TypeContext<'db>) -> Type<'db> {
@@ -8762,7 +8919,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// assignments from the type context to potentially _narrow_ the inferred type,
// by avoiding literal promotion.
let elt_ty_identity = elt_ty.identity(self.db());
let elt_tcx = elt_tcx_constraints.get(&elt_ty_identity).copied();
// If the element is a starred expression, we want to apply the type context to each element
// in the unpacked expression (which we will store as a tuple when inferring it). We
// therefore wrap the type context in an `tuple[T, ...]` specialization.
let elt_tcx = elt_tcx_constraints
.get(&elt_ty_identity)
.copied()
.map(|tcx| {
if elt.is_starred_expr() && collection_class != KnownClass::Dict {
Type::homogeneous_tuple(self.db(), tcx)
} else {
tcx
}
});
let inferred_elt_ty =
infer_elt_expression(self, (i, elt, TypeContext::new(elt_tcx)));
@@ -8781,7 +8951,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let inferred_elt_ty =
inferred_elt_ty.promote_literals(self.db(), TypeContext::new(elt_tcx));
builder.infer(Type::TypeVar(elt_ty), inferred_elt_ty).ok()?;
builder
.infer(
Type::TypeVar(elt_ty),
if elt.is_starred_expr() {
inferred_elt_ty
.iterate(self.db())
.homogeneous_element_type(self.db())
} else {
inferred_elt_ty
},
)
.ok()?;
}
}
@@ -9668,7 +9849,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
fn infer_starred_expression(
&mut self,
starred: &ast::ExprStarred,
tcx: TypeContext<'db>,
) -> Type<'db> {
let ast::ExprStarred {
range: _,
node_index: _,
@@ -9676,17 +9861,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ctx: _,
} = starred;
let iterable_type = self.infer_expression(value, TypeContext::default());
let db = self.db();
let iterable_type = self.infer_expression(value, tcx);
iterable_type
.try_iterate(self.db())
.map(|tuple| tuple.homogeneous_element_type(self.db()))
.try_iterate(db)
.map(|spec| Type::tuple(TupleType::new(db, &spec)))
.unwrap_or_else(|err| {
err.report_diagnostic(&self.context, iterable_type, value.as_ref().into());
err.fallback_element_type(self.db())
});
// TODO
Type::Dynamic(DynamicType::TodoStarredExpression)
Type::homogeneous_tuple(db, err.fallback_element_type(db))
})
}
fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> {
@@ -13138,16 +13321,36 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let value_node = subscript.value.as_ref();
let inferred = match (value_ty, slice_ty) {
(Type::Dynamic(_) | Type::Never, _) => Some(value_ty),
(Type::TypeAlias(alias), _) => Some(self.infer_subscript_expression_types(
subscript,
alias.value_type(self.db()),
slice_ty,
expr_context,
)),
(_, Type::TypeAlias(alias)) => Some(self.infer_subscript_expression_types(
subscript,
value_ty,
alias.value_type(self.db()),
expr_context,
)),
(Type::Union(union), _) => Some(union.map(db, |element| {
self.infer_subscript_expression_types(subscript, *element, slice_ty, expr_context)
})),
(_, Type::Union(union)) => Some(union.map(db, |element| {
self.infer_subscript_expression_types(subscript, value_ty, *element, expr_context)
})),
// TODO: we can map over the intersection and fold the results back into an intersection,
// but we need to make sure we avoid emitting a diagnostic if one positive element has a `__getitem__`
// method but another does not. This means `infer_subscript_expression_types`
// needs to return a `Result` rather than eagerly emitting diagnostics.
(Type::Intersection(_), _) => {
Some(todo_type!("Subscript expressions on intersections"))
(Type::Intersection(_), _) | (_, Type::Intersection(_)) => {
Some(todo_type!("Subscript expressions with intersections"))
}
// Ex) Given `("a", "b", "c", "d")[1]`, return `"b"`
@@ -13227,6 +13430,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}),
(Type::LiteralString, Type::IntLiteral(_) | Type::BooleanLiteral(_)) => {
Some(Type::LiteralString)
}
(Type::LiteralString, Type::NominalInstance(nominal))
if nominal.slice_literal(db).is_some() =>
{
Some(Type::LiteralString)
}
// Ex) Given `b"value"[1]`, return `97` (i.e., `ord(b"a")`)
(Type::BytesLiteral(literal_ty), Type::IntLiteral(i64_int)) => {
i32::try_from(i64_int).ok().map(|i32_int| {
@@ -13358,7 +13571,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Some(todo_type!("Inference of subscript on special form"))
}
_ => None,
(
Type::FunctionLiteral(_)
| Type::WrapperDescriptor(_)
| Type::BoundMethod(_)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
| Type::Callable(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::GenericAlias(_)
| Type::SubclassOf(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
| Type::ProtocolInstance(_)
| Type::PropertyInstance(_)
| Type::EnumLiteral(_)
| Type::BoundSuper(_)
| Type::TypeIs(_)
| Type::TypeGuard(_)
| Type::TypedDict(_)
| Type::NewTypeInstance(_)
| Type::NominalInstance(_)
| Type::SpecialForm(_)
| Type::KnownInstance(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::LiteralString
| Type::TypeVar(_) // TODO: more complex logic required here!
| Type::KnownBoundMethod(_),
_,
) => None,
};
if let Some(inferred) = inferred {

View File

@@ -167,9 +167,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
ast::Expr::StringLiteral(string) => self.infer_string_annotation_expression(string),
// Annotation expressions also get special handling for `*args` and `**kwargs`.
ast::Expr::Starred(starred) => {
TypeAndQualifiers::declared(self.infer_starred_expression(starred))
}
ast::Expr::Starred(starred) => TypeAndQualifiers::declared(
self.infer_starred_expression(starred, TypeContext::default()),
),
ast::Expr::BytesLiteral(bytes) => {
if let Some(builder) = self

View File

@@ -454,13 +454,6 @@ fn merge_constraints_or<'db>(
}
}
fn place_expr(expr: &ast::Expr) -> Option<PlaceExpr> {
match expr {
ast::Expr::Named(named) => PlaceExpr::try_from_expr(named.target.as_ref()),
_ => PlaceExpr::try_from_expr(expr),
}
}
/// Return `true` if it is possible for any two inhabitants of the given types to
/// compare equal to each other; otherwise return `false`.
fn could_compare_equal<'db>(db: &'db dyn Db, left_ty: Type<'db>, right_ty: Type<'db>) -> bool {
@@ -721,7 +714,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
expr: &ast::Expr,
is_positive: bool,
) -> Option<NarrowingConstraints<'db>> {
let target = place_expr(expr)?;
let target = PlaceExpr::try_from_expr(expr)?;
let place = self.expect_place(&target);
let ty = if is_positive {
@@ -1030,15 +1023,15 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
if matches!(&**ops, [ast::CmpOp::Is | ast::CmpOp::IsNot])
&& let ast::Expr::Subscript(subscript) = &**left
&& let Type::Union(union) = inference.expression_type(&*subscript.value)
&& let Some(subscript_place_expr) = place_expr(&subscript.value)
&& let Some(subscript_place_expr) = PlaceExpr::try_from_expr(&subscript.value)
&& let Type::IntLiteral(index) = inference.expression_type(&*subscript.slice)
&& let Ok(index) = i32::try_from(index)
&& let rhs_ty = inference.expression_type(&comparators[0])
&& rhs_ty.is_singleton(self.db)
{
let is_positive_check = is_positive == (ops[0] == ast::CmpOp::Is);
let filtered: Vec<_> = union
.elements(self.db)
let union_elements = union.elements(self.db);
let filtered: Vec<_> = union_elements
.iter()
.filter(|elem| {
elem.as_nominal_instance()
@@ -1056,11 +1049,11 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
})
.copied()
.collect();
if filtered.len() < union.elements(self.db).len() {
if filtered.len() < union_elements.len() {
let place = self.expect_place(&subscript_place_expr);
constraints.insert(
place,
NarrowingConstraint::regular(UnionType::from_elements(self.db, filtered)),
NarrowingConstraint::typeguard(UnionType::from_elements(self.db, filtered)),
);
}
}
@@ -1122,7 +1115,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
// reveal_type(u) # revealed: Bar
if matches!(&**ops, [ast::CmpOp::In | ast::CmpOp::NotIn])
&& let Type::StringLiteral(key) = inference.expression_type(&**left)
&& let Some(rhs_place_expr) = place_expr(&comparators[0])
&& let Some(rhs_place_expr) = PlaceExpr::try_from_expr(&comparators[0])
&& let rhs_type = inference.expression_type(&comparators[0])
&& is_typeddict_or_union_with_typeddicts(self.db, rhs_type)
{
@@ -1190,7 +1183,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
| ast::Expr::Attribute(_)
| ast::Expr::Subscript(_)
| ast::Expr::Named(_) => {
if let Some(left) = place_expr(left)
if let Some(left) = PlaceExpr::try_from_expr(left)
&& let Some(ty) =
self.evaluate_expr_compare_op(lhs_ty, rhs_ty, *op, is_positive)
{
@@ -1215,7 +1208,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
};
let target = match &**args {
[first] => match place_expr(first) {
[first] => match PlaceExpr::try_from_expr(first) {
Some(target) => target,
None => continue,
},
@@ -1244,7 +1237,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
constraints.insert(
place,
NarrowingConstraint::regular(
Type::instance(self.db, rhs_class.unknown_specialization(self.db))
Type::instance(self.db, rhs_class.top_materialization(self.db))
.negate_if(self.db, !is_positive),
),
);
@@ -1264,7 +1257,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
| ast::Expr::Named(_)
) =>
{
if let Some(right_place) = place_expr(right)
if let Some(right_place) = PlaceExpr::try_from_expr(right)
// Swap lhs_ty and rhs_ty since we're narrowing the right operand
&& let Some(ty) =
self.evaluate_expr_compare_op(rhs_ty, lhs_ty, *op, is_positive)
@@ -1315,7 +1308,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
// Narrow only the parts of the type that are safe to narrow based on len().
if let Some(narrowed_ty) = Self::narrow_type_by_len(self.db, arg_ty, is_positive) {
let target = place_expr(arg)?;
let target = PlaceExpr::try_from_expr(arg)?;
let place = self.expect_place(&target);
Some(NarrowingConstraints::from_iter([(
place,
@@ -1329,7 +1322,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
let [first_arg, second_arg] = &*expr_call.arguments.args else {
return None;
};
let first_arg = place_expr(first_arg)?;
let first_arg = PlaceExpr::try_from_expr(first_arg)?;
let function = function_type.known(self.db)?;
let place = self.expect_place(&first_arg);
@@ -1427,7 +1420,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
singleton: ast::Singleton,
is_positive: bool,
) -> Option<NarrowingConstraints<'db>> {
let subject = place_expr(subject.node_ref(self.db, self.module))?;
let subject = PlaceExpr::try_from_expr(subject.node_ref(self.db, self.module))?;
let place = self.expect_place(&subject);
let ty = match singleton {
@@ -1456,7 +1449,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
return None;
}
let subject = place_expr(subject.node_ref(self.db, self.module))?;
let subject = PlaceExpr::try_from_expr(subject.node_ref(self.db, self.module))?;
let place = self.expect_place(&subject);
let class_type =
@@ -1486,7 +1479,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
) -> Option<NarrowingConstraints<'db>> {
let subject_node = subject.node_ref(self.db, self.module);
let place = {
let subject = place_expr(subject_node)?;
let subject = PlaceExpr::try_from_expr(subject_node)?;
self.expect_place(&subject)
};
let subject_ty =
@@ -1638,7 +1631,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
if !is_typeddict_or_union_with_typeddicts(self.db, subscript_value_type) {
return None;
}
let subscript_place_expr = place_expr(subscript_value_expr)?;
let subscript_place_expr = PlaceExpr::try_from_expr(subscript_value_expr)?;
let Type::StringLiteral(key_literal) = subscript_key_type else {
return None;
};
@@ -1724,7 +1717,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
return None;
}
let subscript_place_expr = place_expr(subscript_value_expr)?;
let subscript_place_expr = PlaceExpr::try_from_expr(subscript_value_expr)?;
// Skip narrowing if any tuple in the union has an out-of-bounds index.
// A diagnostic will be emitted elsewhere for the out-of-bounds access.
@@ -1741,8 +1734,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
}
// Filter the union based on whether each tuple element at the index could match the rhs.
let filtered: Vec<_> = union
.elements(self.db)
let union_elements = union.elements(self.db);
let filtered: Vec<_> = union_elements
.iter()
.filter(|elem| {
elem.as_nominal_instance()
@@ -1762,11 +1755,11 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
.collect();
// Only create a constraint if we actually narrowed something.
if filtered.len() < union.elements(self.db).len() {
if filtered.len() < union_elements.len() {
let place = self.expect_place(&subscript_place_expr);
Some((
place,
NarrowingConstraint::regular(UnionType::from_elements(self.db, filtered)),
NarrowingConstraint::typeguard(UnionType::from_elements(self.db, filtered)),
))
} else {
None

View File

@@ -2204,6 +2204,15 @@ pub(crate) struct Parameter<'db> {
/// type semantics of the parameter.
pub(crate) inferred_annotation: bool,
/// Variadic parameters can have starred annotations, e.g.
/// - `*args: *Ts`
/// - `*args: *tuple[int, ...]`
/// - `*args: *tuple[int, *tuple[str, ...], bytes]`
///
/// The `*` prior to the type gives the annotation a different meaning,
/// so this must be propagated upwards.
has_starred_annotation: bool,
kind: ParameterKind<'db>,
pub(crate) form: ParameterForm,
}
@@ -2213,6 +2222,7 @@ impl<'db> Parameter<'db> {
Self {
annotated_type: Type::unknown(),
inferred_annotation: true,
has_starred_annotation: false,
kind: ParameterKind::PositionalOnly {
name,
default_type: None,
@@ -2225,6 +2235,7 @@ impl<'db> Parameter<'db> {
Self {
annotated_type: Type::unknown(),
inferred_annotation: true,
has_starred_annotation: false,
kind: ParameterKind::PositionalOrKeyword {
name,
default_type: None,
@@ -2237,6 +2248,7 @@ impl<'db> Parameter<'db> {
Self {
annotated_type: Type::unknown(),
inferred_annotation: true,
has_starred_annotation: false,
kind: ParameterKind::Variadic { name },
form: ParameterForm::Value,
}
@@ -2246,6 +2258,7 @@ impl<'db> Parameter<'db> {
Self {
annotated_type: Type::unknown(),
inferred_annotation: true,
has_starred_annotation: false,
kind: ParameterKind::KeywordOnly {
name,
default_type: None,
@@ -2258,6 +2271,7 @@ impl<'db> Parameter<'db> {
Self {
annotated_type: Type::unknown(),
inferred_annotation: true,
has_starred_annotation: false,
kind: ParameterKind::KeywordVariadic { name },
form: ParameterForm::Value,
}
@@ -2306,6 +2320,7 @@ impl<'db> Parameter<'db> {
.kind
.apply_type_mapping_impl(db, type_mapping, tcx, visitor),
inferred_annotation: self.inferred_annotation,
has_starred_annotation: self.has_starred_annotation,
form: self.form,
}
}
@@ -2323,7 +2338,8 @@ impl<'db> Parameter<'db> {
annotated_type,
kind,
form,
..
has_starred_annotation,
inferred_annotation: _,
} = self;
// Ensure unions and intersections are ordered in the annotated type.
@@ -2364,6 +2380,7 @@ impl<'db> Parameter<'db> {
// Normalize `inferred_annotation` to `false` since it's a display-only field
// that doesn't affect type semantics.
inferred_annotation: false,
has_starred_annotation: *has_starred_annotation,
kind,
form: *form,
}
@@ -2377,6 +2394,7 @@ impl<'db> Parameter<'db> {
) -> Option<Self> {
let Parameter {
annotated_type,
has_starred_annotation,
inferred_annotation,
kind,
form,
@@ -2437,6 +2455,7 @@ impl<'db> Parameter<'db> {
Some(Self {
annotated_type,
inferred_annotation: *inferred_annotation,
has_starred_annotation: *has_starred_annotation,
kind,
form: *form,
})
@@ -2448,18 +2467,20 @@ impl<'db> Parameter<'db> {
parameter: &ast::Parameter,
kind: ParameterKind<'db>,
) -> Self {
let (annotated_type, inferred_annotation) = if let Some(annotation) = parameter.annotation()
{
(
function_signature_expression_type(db, definition, annotation),
false,
)
} else {
(Type::unknown(), true)
};
let (annotated_type, inferred_annotation, has_starred_annotation) =
if let Some(annotation) = parameter.annotation() {
(
function_signature_expression_type(db, definition, annotation),
false,
annotation.is_starred_expr(),
)
} else {
(Type::unknown(), true, false)
};
Self {
annotated_type,
kind,
has_starred_annotation,
form: ParameterForm::Value,
inferred_annotation,
}
@@ -2511,6 +2532,12 @@ impl<'db> Parameter<'db> {
self.annotated_type
}
/// Return `true` if this parameter has a starred annotation,
/// e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...], bytes]`
pub(crate) fn has_starred_annotation(&self) -> bool {
self.has_starred_annotation
}
/// Kind of the parameter.
pub(crate) fn kind(&self) -> &ParameterKind<'db> {
&self.kind

607
scripts/conformance.py Normal file
View File

@@ -0,0 +1,607 @@
"""
Run typing conformance tests and compare results between two ty versions.
By default, this script will use `uv` to run the latest version of ty
as the new version with `uvx ty@latest`. This requires `uv` to be installed
and available in the system PATH.
If CONFORMANCE_SUITE_COMMIT is set, the hash will be used to create
links to the corresponding line in the conformance repository for each
diagnostic. Otherwise, it will default to `main'.
Examples:
# Compare two specific ty versions
%(prog)s --old-ty uvx ty@0.0.1a35 --new-ty uvx ty@0.0.7
# Use local ty builds
%(prog)s --old-ty ./target/debug/ty-old --new-ty ./target/debug/ty-new
# Custom test directory
%(prog)s --target-path custom/tests --old-ty uvx ty@0.0.1a35 --new-ty uvx ty@0.0.7
# Show all diagnostics (not just changed ones)
%(prog)s --all --old-ty uvx ty@0.0.1a35 --new-ty uvx ty@0.0.7
# Show a diff with local paths to the test directory instead of table of links
%(prog)s --old-ty uvx ty@0.0.1a35 --new-ty uvx ty@0.0.7 --format diff
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from enum import Flag, StrEnum, auto
from functools import reduce
from itertools import groupby
from operator import attrgetter, or_
from pathlib import Path
from textwrap import dedent
from typing import Any, Literal, Self
# The conformance tests include 4 types of errors:
# 1. Required errors (E): The type checker must raise an error on this line
# 2. Optional errors (E?): The type checker may raise an error on this line
# 3. Tagged errors (E[tag]): The type checker must raise at most one error on any of the lines in a file with matching tags
# 4. Tagged multi-errors (E[tag+]): The type checker should raise one or more errors on any of the tagged lines
# This regex pattern parses the error lines in the conformance tests, but the following
# implementation treats all errors as required errors.
CONFORMANCE_ERROR_PATTERN = re.compile(
r"""
\#\s*E # "# E" begins each error
(?P<optional>\?)? # Optional '?' (E?) indicates that an error is optional
(?: # An optional tag for errors that may appear on multiple lines at most once
\[
(?P<tag>[^+\]]+) # identifier
(?P<multi>\+)? # '+' indicates that an error may occur more than once on tagged lines
\]
)?
(?:
\s*:\s*(?P<description>.*) # optional description
)?
""",
re.VERBOSE,
)
CONFORMANCE_URL = "https://github.com/python/typing/blob/{conformance_suite_commit}/conformance/tests/{filename}#L{line}"
CONFORMANCE_SUITE_COMMIT = os.environ.get("CONFORMANCE_SUITE_COMMIT", "main")
class Source(Flag):
OLD = auto()
NEW = auto()
EXPECTED = auto()
class Classification(StrEnum):
TRUE_POSITIVE = auto()
FALSE_POSITIVE = auto()
TRUE_NEGATIVE = auto()
FALSE_NEGATIVE = auto()
def into_title(self) -> str:
match self:
case Classification.TRUE_POSITIVE:
return "True positives added 🎉"
case Classification.FALSE_POSITIVE:
return "False positives added 🫤"
case Classification.TRUE_NEGATIVE:
return "False positives removed 🎉"
case Classification.FALSE_NEGATIVE:
return "True positives removed 🫤"
@dataclass(kw_only=True, slots=True)
class Position:
line: int
column: int
@dataclass(kw_only=True, slots=True)
class Positions:
begin: Position
end: Position
@dataclass(kw_only=True, slots=True)
class Location:
path: str
positions: Positions
def as_link(self) -> str:
file = os.path.basename(self.path)
link = CONFORMANCE_URL.format(
conformance_suite_commit=CONFORMANCE_SUITE_COMMIT,
filename=file,
line=self.positions.begin.line,
)
return f"[{file}:{self.positions.begin.line}:{self.positions.begin.column}]({link})"
@dataclass(kw_only=True, slots=True)
class Diagnostic:
check_name: str
description: str
severity: str
fingerprint: str | None
location: Location
source: Source
def __str__(self) -> str:
return (
f"{self.location.path}:{self.location.positions.begin.line}:"
f"{self.location.positions.begin.column}: "
f"{self.severity_for_display}[{self.check_name}] {self.description}"
)
@classmethod
def from_gitlab_output(
cls,
dct: dict[str, Any],
source: Source,
) -> Self:
return cls(
check_name=dct["check_name"],
description=dct["description"],
severity=dct["severity"],
fingerprint=dct["fingerprint"],
location=Location(
path=dct["location"]["path"],
positions=Positions(
begin=Position(
line=dct["location"]["positions"]["begin"]["line"],
column=dct["location"]["positions"]["begin"]["column"],
),
end=Position(
line=dct["location"]["positions"]["end"]["line"],
column=dct["location"]["positions"]["end"]["column"],
),
),
),
source=source,
)
@property
def key(self) -> str:
"""Key to group diagnostics by path and beginning line."""
return f"{self.location.path}:{self.location.positions.begin.line}"
@property
def severity_for_display(self) -> str:
return {
"major": "error",
"minor": "warning",
}.get(self.severity, "unknown")
@dataclass(kw_only=True, slots=True)
class GroupedDiagnostics:
key: str
sources: Source
old: Diagnostic | None
new: Diagnostic | None
expected: Diagnostic | None
@property
def changed(self) -> bool:
return (Source.OLD in self.sources or Source.NEW in self.sources) and not (
Source.OLD in self.sources and Source.NEW in self.sources
)
@property
def classification(self) -> Classification:
if Source.NEW in self.sources and Source.EXPECTED in self.sources:
return Classification.TRUE_POSITIVE
elif Source.NEW in self.sources and Source.EXPECTED not in self.sources:
return Classification.FALSE_POSITIVE
elif Source.EXPECTED in self.sources:
return Classification.FALSE_NEGATIVE
else:
return Classification.TRUE_NEGATIVE
def _render_row(self, diagnostic: Diagnostic):
return f"| {diagnostic.location.as_link()} | {diagnostic.check_name} | {diagnostic.description} |"
def _render_diff(self, diagnostic: Diagnostic, *, removed: bool = False):
sign = "-" if removed else "+"
return f"{sign} {diagnostic}"
def display(self, format: Literal["diff", "github"]) -> str:
match self.classification:
case Classification.TRUE_POSITIVE | Classification.FALSE_POSITIVE:
assert self.new is not None
return (
self._render_diff(self.new)
if format == "diff"
else self._render_row(self.new)
)
case Classification.FALSE_NEGATIVE | Classification.TRUE_NEGATIVE:
diagnostic = self.old or self.expected
assert diagnostic is not None
return (
self._render_diff(diagnostic, removed=True)
if format == "diff"
else self._render_row(diagnostic)
)
case _:
raise ValueError(f"Unexpected classification: {self.classification}")
@dataclass(kw_only=True, slots=True)
class Statistics:
true_positives: int = 0
false_positives: int = 0
false_negatives: int = 0
@property
def precision(self) -> float:
if self.true_positives + self.false_positives > 0:
return self.true_positives / (self.true_positives + self.false_positives)
return 0.0
@property
def recall(self) -> float:
if self.true_positives + self.false_negatives > 0:
return self.true_positives / (self.true_positives + self.false_negatives)
else:
return 0.0
@property
def total(self) -> int:
return self.true_positives + self.false_positives
def collect_expected_diagnostics(path: Path) -> list[Diagnostic]:
diagnostics: list[Diagnostic] = []
for file in path.resolve().rglob("*.py"):
for idx, line in enumerate(file.read_text().splitlines(), 1):
if error := re.search(CONFORMANCE_ERROR_PATTERN, line):
diagnostics.append(
Diagnostic(
check_name="conformance",
description=error.group("description")
or error.group("tag")
or "Missing",
severity="major",
fingerprint=None,
location=Location(
path=file.as_posix(),
positions=Positions(
begin=Position(
line=idx,
column=error.start(),
),
end=Position(
line=idx,
column=error.end(),
),
),
),
source=Source.EXPECTED,
)
)
assert diagnostics, "Failed to discover any expected diagnostics!"
return diagnostics
def collect_ty_diagnostics(
ty_path: list[str],
source: Source,
tests_path: str = ".",
python_version: str = "3.12",
) -> list[Diagnostic]:
process = subprocess.run(
[
*ty_path,
"check",
f"--python-version={python_version}",
"--output-format=gitlab",
"--exit-zero",
tests_path,
],
capture_output=True,
text=True,
check=True,
timeout=15,
)
if process.returncode != 0:
print(process.stderr)
raise RuntimeError(f"ty check failed with exit code {process.returncode}")
return [
Diagnostic.from_gitlab_output(dct, source=source)
for dct in json.loads(process.stdout)
]
def group_diagnostics_by_key(
old: list[Diagnostic], new: list[Diagnostic], expected: list[Diagnostic]
) -> list[GroupedDiagnostics]:
diagnostics = [
*old,
*new,
*expected,
]
sorted_diagnostics = sorted(diagnostics, key=attrgetter("key"))
grouped = []
for key, group in groupby(sorted_diagnostics, key=attrgetter("key")):
group = list(group)
sources: Source = reduce(or_, (diag.source for diag in group))
grouped.append(
GroupedDiagnostics(
key=key,
sources=sources,
old=next(filter(lambda diag: diag.source == Source.OLD, group), None),
new=next(filter(lambda diag: diag.source == Source.NEW, group), None),
expected=next(
filter(lambda diag: diag.source == Source.EXPECTED, group), None
),
)
)
return grouped
def compute_stats(
grouped_diagnostics: list[GroupedDiagnostics], source: Source
) -> Statistics:
if source == source.EXPECTED:
# ty currently raises a false positive here due to incomplete enum.Flag support
# see https://github.com/astral-sh/ty/issues/876
num_errors = sum(
1
for g in grouped_diagnostics
if source.EXPECTED in g.sources # ty:ignore[unsupported-operator]
)
return Statistics(
true_positives=num_errors, false_positives=0, false_negatives=0
)
def increment(statistics: Statistics, grouped: GroupedDiagnostics) -> Statistics:
if (source in grouped.sources) and (Source.EXPECTED in grouped.sources):
statistics.true_positives += 1
elif source in grouped.sources:
statistics.false_positives += 1
elif Source.EXPECTED in grouped.sources:
statistics.false_negatives += 1
return statistics
return reduce(increment, grouped_diagnostics, Statistics())
def render_grouped_diagnostics(
grouped: list[GroupedDiagnostics],
*,
changed_only: bool = True,
format: Literal["diff", "github"] = "diff",
) -> str:
if changed_only:
grouped = [diag for diag in grouped if diag.changed]
sorted_by_class = sorted(
grouped,
key=attrgetter("classification"),
reverse=True,
)
match format:
case "diff":
header = ["```diff"]
footer = "```"
case "github":
header = [
"| Location | Name | Message |",
"|----------|------|---------|",
]
footer = ""
case _:
raise ValueError("format must be one of 'diff' or 'github'")
lines = []
for classification, group in groupby(
sorted_by_class, key=attrgetter("classification")
):
group = list(group)
lines.append(f"## {classification.into_title()}")
lines.extend(["", "<details>", ""])
lines.extend(header)
for diag in group:
lines.append(diag.display(format=format))
lines.append(footer)
lines.extend(["", "</details>", ""])
return "\n".join(lines)
def diff_format(
diff: float,
*,
greater_is_better: bool = True,
neutral: bool = False,
is_percentage: bool = False,
):
increased = diff > 0
good = " (✅)" if not neutral else ""
bad = " (❌)" if not neutral else ""
up = ""
down = ""
match (greater_is_better, increased):
case (True, True):
return f"{up}{good}"
case (False, True):
return f"{up}{bad}"
case (True, False):
return f"{down}{bad}"
case (False, False):
return f"{down}{good}"
def render_summary(grouped_diagnostics: list[GroupedDiagnostics]):
def format_metric(diff: float, old: float, new: float):
if diff > 0:
return f"increased from {old:.2%} to {new:.2%}"
if diff < 0:
return f"decreased from {old:.2%} to {new:.2%}"
return f"held steady at {old:.2%}"
old = compute_stats(grouped_diagnostics, source=Source.OLD)
new = compute_stats(grouped_diagnostics, source=Source.NEW)
precision_change = new.precision - old.precision
recall_change = new.recall - old.recall
true_pos_change = new.true_positives - old.true_positives
false_pos_change = new.false_positives - old.false_positives
false_neg_change = new.false_negatives - old.false_negatives
total_change = new.total - old.total
true_pos_diff = diff_format(true_pos_change, greater_is_better=True)
false_pos_diff = diff_format(false_pos_change, greater_is_better=False)
false_neg_diff = diff_format(false_neg_change, greater_is_better=False)
precision_diff = diff_format(
precision_change, greater_is_better=True, is_percentage=True
)
recall_diff = diff_format(recall_change, greater_is_better=True, is_percentage=True)
total_diff = diff_format(total_change, neutral=True)
table = dedent(
f"""
## Typing Conformance
### Summary
| Metric | Old | New | Diff | Outcome |
|--------|-----|-----|------|---------|
| True Positives | {old.true_positives} | {new.true_positives} | {true_pos_change:+} | {true_pos_diff} |
| False Positives | {old.false_positives} | {new.false_positives} | {false_pos_change:+} | {false_pos_diff} |
| False Negatives | {old.false_negatives} | {new.false_negatives} | {false_neg_change:+} | {false_neg_diff} |
| Total Diagnostics | {old.total} | {new.total} | {total_change} | {total_diff} |
| Precision | {old.precision:.2%} | {new.precision:.2%} | {precision_change:+.2%} | {precision_diff} |
| Recall | {old.recall:.2%} | {new.recall:.2%} | {recall_change:+.2%} | {recall_diff} |
"""
)
summary = (
f"The percentage of diagnostics emitted that were expected errors"
f" {format_metric(precision_change, old.precision, new.precision)},"
" and the percentage of expected errors that received a diagnostic"
f" {format_metric(recall_change, old.recall, new.recall)}."
)
return "\n".join([table, summary])
def parse_args():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--old-ty",
nargs="+",
help="Command to run old version of ty (default: uvx ty@0.0.1a35)",
)
parser.add_argument(
"--new-ty",
nargs="+",
default=["uvx", "ty@latest"],
help="Command to run new version of ty (default: uvx ty@0.0.7)",
)
parser.add_argument(
"--tests-path",
type=Path,
default=Path("typing/conformance/tests"),
help="Path to conformance tests directory (default: typing/conformance/tests)",
)
parser.add_argument(
"--python-version",
type=str,
default="3.12",
help="Python version to assume when running ty (default: 3.12)",
)
parser.add_argument(
"--all",
action="store_true",
help="Show all diagnostics, not just changed ones",
)
parser.add_argument(
"--format", type=str, choices=["diff", "github"], default="github"
)
parser.add_argument(
"--output",
type=Path,
help="Write output to file instead of stdout",
)
args = parser.parse_args()
if args.old_ty is None:
raise ValueError("old_ty is required")
return args
def main():
args = parse_args()
expected = collect_expected_diagnostics(args.tests_path)
old = collect_ty_diagnostics(
ty_path=args.old_ty,
tests_path=str(args.tests_path),
source=Source.OLD,
python_version=args.python_version,
)
new = collect_ty_diagnostics(
ty_path=args.new_ty,
tests_path=str(args.tests_path),
source=Source.NEW,
python_version=args.python_version,
)
grouped = group_diagnostics_by_key(
old=old,
new=new,
expected=expected,
)
rendered = "\n\n".join(
[
render_summary(grouped),
render_grouped_diagnostics(
grouped, changed_only=not args.all, format=args.format
),
]
)
if args.output:
args.output.write_text(rendered, encoding="utf-8")
print(f"Output written to {args.output}", file=sys.stderr)
else:
print(rendered)
if __name__ == "__main__":
main()

10
ty.schema.json generated
View File

@@ -640,6 +640,16 @@
}
]
},
"invalid-generic-enum": {
"title": "detects generic enum classes",
"description": "## What it does\nChecks for enum classes that are also generic.\n\n## Why is this bad?\nEnum classes cannot be generic. Python does not support generic enums:\nattempting to create one will either result in an immediate `TypeError`\nat runtime, or will create a class that cannot be specialized in the way\nthat a normal generic class can.\n\n## Examples\n```python\nfrom enum import Enum\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\n\n# error: enum class cannot be generic (class creation fails with `TypeError`)\nclass E[T](Enum):\n A = 1\n\n# error: enum class cannot be generic (class creation fails with `TypeError`)\nclass F(Enum, Generic[T]):\n A = 1\n\n# error: enum class cannot be generic -- the class creation does not immediately fail...\nclass G(Generic[T], Enum):\n A = 1\n\n# ...but this raises `KeyError`:\nx: G[int]\n```\n\n## References\n- [Python documentation: Enum](https://docs.python.org/3/library/enum.html)",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"invalid-ignore-comment": {
"title": "detects ignore comments that use invalid syntax",
"description": "## What it does\nChecks for `type: ignore` and `ty: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```",