diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6cec5a828f..abee17bfc1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -60,7 +60,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
persist-credentials: false
submodules: recursive
@@ -123,7 +123,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
persist-credentials: false
submodules: recursive
@@ -174,7 +174,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
persist-credentials: false
submodules: recursive
@@ -250,7 +250,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
persist-credentials: false
submodules: recursive
diff --git a/Cargo.toml b/Cargo.toml
index dbd9808fdd..bd06571cd3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# Please update rustfmt.toml when bumping the Rust edition
edition = "2024"
-rust-version = "1.89"
+rust-version = "1.90"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs
index 370186a0b4..da6d3833b7 100644
--- a/crates/ruff/src/args.rs
+++ b/crates/ruff/src/args.rs
@@ -10,7 +10,7 @@ use anyhow::bail;
use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects};
use clap::builder::{TypedValueParser, ValueParserFactory};
-use clap::{Parser, Subcommand, command};
+use clap::{Parser, Subcommand};
use colored::Colorize;
use itertools::Itertools;
use path_absolutize::path_dedot;
diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs
index 3ea0d94fad..3ecefd6dde 100644
--- a/crates/ruff/src/lib.rs
+++ b/crates/ruff/src/lib.rs
@@ -9,7 +9,7 @@ use std::sync::mpsc::channel;
use anyhow::Result;
use clap::CommandFactory;
use colored::Colorize;
-use log::{error, warn};
+use log::error;
use notify::{RecursiveMode, Watcher, recommended_watcher};
use args::{GlobalConfigArgs, ServerCommand};
diff --git a/crates/ruff_dev/src/generate_ty_options.rs b/crates/ruff_dev/src/generate_ty_options.rs
index 4e4ab0a949..733af8b00a 100644
--- a/crates/ruff_dev/src/generate_ty_options.rs
+++ b/crates/ruff_dev/src/generate_ty_options.rs
@@ -144,8 +144,8 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S
output.push('\n');
if let Some(deprecated) = &field.deprecated {
- output.push_str("> [!WARN] \"Deprecated\"\n");
- output.push_str("> This option has been deprecated");
+ output.push_str("!!! warning \"Deprecated\"\n");
+ output.push_str(" This option has been deprecated");
if let Some(since) = deprecated.since {
write!(output, " in {since}").unwrap();
diff --git a/crates/ruff_diagnostics/src/edit.rs b/crates/ruff_diagnostics/src/edit.rs
index 194b4e4494..ae52088608 100644
--- a/crates/ruff_diagnostics/src/edit.rs
+++ b/crates/ruff_diagnostics/src/edit.rs
@@ -39,7 +39,7 @@ impl Edit {
/// Creates an edit that replaces the content in `range` with `content`.
pub fn range_replacement(content: String, range: TextRange) -> Self {
- debug_assert!(!content.is_empty(), "Prefer `Fix::deletion`");
+ debug_assert!(!content.is_empty(), "Prefer `Edit::deletion`");
Self {
content: Some(Box::from(content)),
diff --git a/crates/ruff_formatter/src/macros.rs b/crates/ruff_formatter/src/macros.rs
index 4d0d3ef234..8090d41397 100644
--- a/crates/ruff_formatter/src/macros.rs
+++ b/crates/ruff_formatter/src/macros.rs
@@ -337,7 +337,7 @@ macro_rules! best_fitting {
#[cfg(test)]
mod tests {
use crate::prelude::*;
- use crate::{FormatState, SimpleFormatOptions, VecBuffer, write};
+ use crate::{FormatState, SimpleFormatOptions, VecBuffer};
struct TestFormat;
@@ -385,8 +385,8 @@ mod tests {
#[test]
fn best_fitting_variants_print_as_lists() {
+ use crate::Formatted;
use crate::prelude::*;
- use crate::{Formatted, format, format_args};
// The second variant below should be selected when printing at a width of 30
let formatted_best_fitting = format!(
diff --git a/crates/ruff_linter/src/fix/edits.rs b/crates/ruff_linter/src/fix/edits.rs
index 20f50d6e10..b5420e4ee1 100644
--- a/crates/ruff_linter/src/fix/edits.rs
+++ b/crates/ruff_linter/src/fix/edits.rs
@@ -286,12 +286,7 @@ pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Token
/// Generic function to add a (regular) parameter to a function definition.
pub(crate) fn add_parameter(parameter: &str, parameters: &Parameters, source: &str) -> Edit {
- if let Some(last) = parameters
- .args
- .iter()
- .filter(|arg| arg.default.is_none())
- .next_back()
- {
+ if let Some(last) = parameters.args.iter().rfind(|arg| arg.default.is_none()) {
// Case 1: at least one regular parameter, so append after the last one.
Edit::insertion(format!(", {parameter}"), last.end())
} else if !parameters.args.is_empty() {
diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs
index 687729c20c..84aff5ce5e 100644
--- a/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs
+++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/yoda_conditions.rs
@@ -146,7 +146,7 @@ fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Resu
let left = (*comparison.left).clone();
// Copy the right side to the left side.
- comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
+ *comparison.left = comparison.comparisons[0].comparator.clone();
// Copy the left side to the right side.
comparison.comparisons[0].comparator = left;
diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs
index 710e295f62..362d00d235 100644
--- a/crates/ruff_python_codegen/src/generator.rs
+++ b/crates/ruff_python_codegen/src/generator.rs
@@ -1247,6 +1247,7 @@ impl<'a> Generator<'a> {
self.p_bytes_repr(&bytes_literal.value, bytes_literal.flags);
}
}
+ #[expect(clippy::eq_op)]
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
static INF_STR: &str = "1e309";
assert_eq!(f64::MAX_10_EXP, 308);
diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs
index 96da64e86e..df289f08d8 100644
--- a/crates/ruff_python_formatter/src/cli.rs
+++ b/crates/ruff_python_formatter/src/cli.rs
@@ -3,7 +3,7 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
-use clap::{Parser, ValueEnum, command};
+use clap::{Parser, ValueEnum};
use ruff_formatter::SourceCode;
use ruff_python_ast::{PySourceType, PythonVersion};
diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md
index 0768f1b26a..edbea9c803 100644
--- a/crates/ty/docs/configuration.md
+++ b/crates/ty/docs/configuration.md
@@ -432,8 +432,8 @@ respect-ignore-files = false
### `root`
-> [!WARN] "Deprecated"
-> This option has been deprecated. Use `environment.root` instead.
+!!! warning "Deprecated"
+ This option has been deprecated. Use `environment.root` instead.
The root of the project, used for finding first-party modules.
diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index c2b450ae10..8b35be52a6 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -557,7 +557,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -751,7 +751,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -793,7 +793,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.35 ·
Related issues ·
-View source
+View source
@@ -848,16 +848,21 @@ Checks for the creation of invalid generic classes
**Why is this bad?**
There are several requirements that you must follow when defining a generic class.
+Many of these result in `TypeError` being raised at runtime if they are violated.
**Examples**
```python
-from typing import Generic, TypeVar
+from typing_extensions import Generic, TypeVar
-T = TypeVar("T") # okay
+T = TypeVar("T")
+U = TypeVar("U", default=int)
# error: class uses both PEP-695 syntax and legacy syntax
class C[U](Generic[T]): ...
+
+# error: type parameter with default comes before type parameter without default
+class D(Generic[U, T]): ...
```
**References**
@@ -909,7 +914,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -944,7 +949,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -978,7 +983,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1139,7 +1144,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1169,7 +1174,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1219,7 +1224,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1245,7 +1250,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1310,7 +1315,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1384,7 +1389,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1442,7 +1447,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1469,7 +1474,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -1516,7 +1521,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1546,7 +1551,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1576,7 +1581,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1610,7 +1615,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1644,7 +1649,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1679,7 +1684,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1704,7 +1709,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1737,7 +1742,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1766,7 +1771,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1790,7 +1795,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1816,7 +1821,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -1849,7 +1854,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1876,7 +1881,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1934,7 +1939,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1964,7 +1969,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1993,7 +1998,7 @@ class B(A): ... # Error raised here
Default level: error ·
Preview (since 0.0.1-alpha.30) ·
Related issues ·
-View source
+View source
@@ -2027,7 +2032,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2054,7 +2059,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2082,7 +2087,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2128,7 +2133,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2155,7 +2160,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2183,7 +2188,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2208,7 +2213,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2233,7 +2238,7 @@ print(x) # NameError: name 'x' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2270,7 +2275,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2298,7 +2303,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2452,7 +2457,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2512,7 +2517,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2544,7 +2549,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2571,7 +2576,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2595,7 +2600,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2692,7 +2697,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2779,7 +2784,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs
index ca2305df0c..4e5051ddcc 100644
--- a/crates/ty_ide/src/completion.rs
+++ b/crates/ty_ide/src/completion.rs
@@ -4711,8 +4711,7 @@ from os.
let last_nonunderscore = test
.completions()
.iter()
- .filter(|c| !c.name.starts_with('_'))
- .next_back()
+ .rfind(|c| !c.name.starts_with('_'))
.unwrap();
assert_eq!(&last_nonunderscore.name, "type_check_only");
diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs
index fe034b0a07..2bcb287b40 100644
--- a/crates/ty_project/src/lib.rs
+++ b/crates/ty_project/src/lib.rs
@@ -27,7 +27,6 @@ use std::iter::FusedIterator;
use std::panic::{AssertUnwindSafe, UnwindSafe};
use std::sync::Arc;
use thiserror::Error;
-use tracing::error;
use ty_python_semantic::add_inferred_python_version_hint_to_diagnostic;
use ty_python_semantic::lint::RuleSelection;
use ty_python_semantic::types::check_types;
diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md
new file mode 100644
index 0000000000..705ceae6b6
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md
@@ -0,0 +1,43 @@
+# Invalid Order of Legacy Type Parameters
+
+
+
+```toml
+[environment]
+python-version = "3.13"
+```
+
+```py
+from typing import TypeVar, Generic, Protocol
+
+T1 = TypeVar("T1", default=int)
+
+T2 = TypeVar("T2")
+T3 = TypeVar("T3")
+
+DefaultStrT = TypeVar("DefaultStrT", default=str)
+
+class SubclassMe(Generic[T1, DefaultStrT]):
+ x: DefaultStrT
+
+class Baz(SubclassMe[int, DefaultStrT]):
+ pass
+
+# error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
+class Foo(Generic[T1, T2]):
+ pass
+
+class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
+ pass
+
+class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+ pass
+
+class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+ pass
+
+class VeryBad(
+ Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
+ Generic[T1, T2, DefaultStrT, T3],
+): ...
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md
index 7a365b6405..5e3cbe3888 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md
@@ -424,9 +424,8 @@ p3 = ParamSpecWithDefault4[[int], [str]]()
reveal_type(p3.attr1) # revealed: (int, /) -> None
reveal_type(p3.attr2) # revealed: (str, /) -> None
-# TODO: error
# Un-ordered type variables as the default of `PAnother` is `P`
-class ParamSpecWithDefault5(Generic[PAnother, P]):
+class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-generic-class]
attr: Callable[PAnother, None]
# TODO: error
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md
index 354521288e..e6e6acd35c 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md
@@ -670,3 +670,59 @@ reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str,
# error: [invalid-argument-type]
reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str]
```
+
+## ParamSpec attribute assignability
+
+When comparing signatures with `ParamSpec` attributes (`P.args` and `P.kwargs`), two different
+inferable `ParamSpec` attributes with the same kind are assignable to each other. This enables
+method overrides where both methods have their own `ParamSpec`.
+
+### Same attribute kind, both inferable
+
+```py
+from typing import Callable
+
+class Parent:
+ def method[**P](self, callback: Callable[P, None]) -> Callable[P, None]:
+ return callback
+
+class Child1(Parent):
+ # This is a valid override: Q.args matches P.args, Q.kwargs matches P.kwargs
+ def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
+ return callback
+
+# Both signatures use ParamSpec, so they should be compatible
+def outer[**P](f: Callable[P, int]) -> Callable[P, int]:
+ def inner[**Q](g: Callable[Q, int]) -> Callable[Q, int]:
+ return g
+ return inner(f)
+```
+
+We can explicitly mark it as an override using the `@override` decorator.
+
+```py
+from typing import override
+
+class Child2(Parent):
+ @override
+ def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
+ return callback
+```
+
+### One `ParamSpec` not inferable
+
+Here, `P` is in a non-inferable position while `Q` is inferable. So, they are not considered
+assignable.
+
+```py
+from typing import Callable
+
+class Container[**P]:
+ def method(self, f: Callable[P, None]) -> Callable[P, None]:
+ return f
+
+ def try_assign[**Q](self, f: Callable[Q, None]) -> Callable[Q, None]:
+ # error: [invalid-return-type] "Return type does not match returned value: expected `(**Q@try_assign) -> None`, found `(**P@Container) -> None`"
+ # error: [invalid-argument-type] "Argument to bound method `method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`"
+ return self.method(f)
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md
index f74cafb80b..f2fd9b7595 100644
--- a/crates/ty_python_semantic/resources/mdtest/overloads.md
+++ b/crates/ty_python_semantic/resources/mdtest/overloads.md
@@ -418,6 +418,18 @@ Using the `@abstractmethod` decorator requires that the class's metaclass is `AB
from it.
```py
+from abc import ABCMeta
+
+class CustomAbstractMetaclass(ABCMeta): ...
+
+class Fine(metaclass=CustomAbstractMetaclass):
+ @overload
+ @abstractmethod
+ def f(self, x: int) -> int: ...
+ @overload
+ @abstractmethod
+ def f(self, x: str) -> str: ...
+
class Foo:
@overload
@abstractmethod
@@ -448,6 +460,52 @@ class PartialFoo(ABC):
def f(self, x: str) -> str: ...
```
+#### `TYPE_CHECKING` blocks
+
+As in other areas of ty, we treat `TYPE_CHECKING` blocks the same as "inline stub files", so we
+permit overloaded functions to exist without an implementation if all overloads are defined inside
+an `if TYPE_CHECKING` block:
+
+```py
+from typing import overload, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ @overload
+ def a() -> str: ...
+ @overload
+ def a(x: int) -> int: ...
+
+ class F:
+ @overload
+ def method(self) -> None: ...
+ @overload
+ def method(self, x: int) -> int: ...
+
+class G:
+ if TYPE_CHECKING:
+ @overload
+ def method(self) -> None: ...
+ @overload
+ def method(self, x: int) -> int: ...
+
+if TYPE_CHECKING:
+ @overload
+ def b() -> str: ...
+
+if TYPE_CHECKING:
+ @overload
+ def b(x: int) -> int: ...
+
+if TYPE_CHECKING:
+ @overload
+ def c() -> None: ...
+
+# not all overloads are in a `TYPE_CHECKING` block, so this is an error
+@overload
+# error: [invalid-overload]
+def c(x: int) -> int: ...
+```
+
### `@overload`-decorated functions with non-stub bodies
diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md
new file mode 100644
index 0000000000..7130538acf
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md
@@ -0,0 +1,120 @@
+# Implicit class body attributes
+
+## Class body implicit attributes
+
+Python makes certain names available implicitly inside class body scopes. These are `__qualname__`,
+`__module__`, and `__doc__`, as documented at
+.
+
+```py
+class Foo:
+ reveal_type(__qualname__) # revealed: str
+ reveal_type(__module__) # revealed: str
+ reveal_type(__doc__) # revealed: str | None
+```
+
+## `__firstlineno__` (Python 3.13+)
+
+Python 3.13 added `__firstlineno__` to the class body namespace.
+
+### Available in Python 3.13+
+
+```toml
+[environment]
+python-version = "3.13"
+```
+
+```py
+class Foo:
+ reveal_type(__firstlineno__) # revealed: int
+```
+
+### Not available in Python 3.12 and earlier
+
+```toml
+[environment]
+python-version = "3.12"
+```
+
+```py
+class Foo:
+ # error: [unresolved-reference]
+ __firstlineno__
+```
+
+## Nested classes
+
+These implicit attributes are also available in nested classes, and refer to the nested class:
+
+```py
+class Outer:
+ class Inner:
+ reveal_type(__qualname__) # revealed: str
+ reveal_type(__module__) # revealed: str
+```
+
+## Class body implicit attributes have priority over globals
+
+If a global variable with the same name exists, the class body implicit attribute takes priority
+within the class body:
+
+```py
+__qualname__ = 42
+__module__ = 42
+
+class Foo:
+ # Inside the class body, these are the implicit class attributes
+ reveal_type(__qualname__) # revealed: str
+ reveal_type(__module__) # revealed: str
+
+# Outside the class, the globals are visible
+reveal_type(__qualname__) # revealed: Literal[42]
+reveal_type(__module__) # revealed: Literal[42]
+```
+
+## `__firstlineno__` has priority over globals (Python 3.13+)
+
+The same applies to `__firstlineno__` on Python 3.13+:
+
+```toml
+[environment]
+python-version = "3.13"
+```
+
+```py
+__firstlineno__ = "not an int"
+
+class Foo:
+ reveal_type(__firstlineno__) # revealed: int
+
+reveal_type(__firstlineno__) # revealed: Literal["not an int"]
+```
+
+## Class body implicit attributes are not visible in methods
+
+The implicit class body attributes are only available directly in the class body, not in nested
+function scopes (methods):
+
+```py
+class Foo:
+ # Available directly in the class body
+ x = __qualname__
+ reveal_type(x) # revealed: str
+
+ def method(self):
+ # Not available in methods - falls back to builtins/globals
+ # error: [unresolved-reference]
+ __qualname__
+```
+
+## Real-world use case: logging
+
+A common use case is defining a logger with the class name:
+
+```py
+import logging
+
+class MyClass:
+ logger = logging.getLogger(__qualname__)
+ reveal_type(logger) # revealed: Logger
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet…_-_Invalid_Order_of_Leg…_(eaa359e8d6b3031d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet…_-_Invalid_Order_of_Leg…_(eaa359e8d6b3031d).snap
new file mode 100644
index 0000000000..290fbb0ae0
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet…_-_Invalid_Order_of_Leg…_(eaa359e8d6b3031d).snap
@@ -0,0 +1,190 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+---
+mdtest name: invalid_type_parameter_order.md - Invalid Order of Legacy Type Parameters
+mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md
+---
+
+# Python source files
+
+## mdtest_snippet.py
+
+```
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ 6 | T3 = TypeVar("T3")
+ 7 |
+ 8 | DefaultStrT = TypeVar("DefaultStrT", default=str)
+ 9 |
+10 | class SubclassMe(Generic[T1, DefaultStrT]):
+11 | x: DefaultStrT
+12 |
+13 | class Baz(SubclassMe[int, DefaultStrT]):
+14 | pass
+15 |
+16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
+17 | class Foo(Generic[T1, T2]):
+18 | pass
+19 |
+20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
+21 | pass
+22 |
+23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+24 | pass
+25 |
+26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+27 | pass
+28 |
+29 | class VeryBad(
+30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
+31 | Generic[T1, T2, DefaultStrT, T3],
+32 | ): ...
+```
+
+# Diagnostics
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:17:19
+ |
+16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
+17 | class Foo(Generic[T1, T2]):
+ | ^^^^^^
+ | |
+ | Type variable `T2` does not have a default
+ | Earlier TypeVar `T1` does
+18 | pass
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ | ------------------ `T2` defined here
+ 6 | T3 = TypeVar("T3")
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:20:19
+ |
+18 | pass
+19 |
+20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
+ | ^^^^^^^^^^
+ | |
+ | Type variable `T3` does not have a default
+ | Earlier TypeVar `T1` does
+21 | pass
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ 6 | T3 = TypeVar("T3")
+ | ------------------ `T3` defined here
+ 7 |
+ 8 | DefaultStrT = TypeVar("DefaultStrT", default=str)
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:23:20
+ |
+21 | pass
+22 |
+23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+ | ^^^^^^^^^^^^^^^^^^^^^^^
+ | |
+ | Type variables `T2` and `T3` do not have defaults
+ | Earlier TypeVar `T1` does
+24 | pass
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ | ------------------ `T2` defined here
+ 6 | T3 = TypeVar("T3")
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:26:20
+ |
+24 | pass
+25 |
+26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
+ | ^^^^^^^^^^^^^^^^^^^^^^^
+ | |
+ | Type variables `T2` and `T3` do not have defaults
+ | Earlier TypeVar `T1` does
+27 | pass
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ | ------------------ `T2` defined here
+ 6 | T3 = TypeVar("T3")
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
+
+```
+error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
+ --> src/mdtest_snippet.py:30:14
+ |
+29 | class VeryBad(
+30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
+ | ^^^^^^^^^^^^^^^^^^^^^^^
+ | |
+ | Type variables `T2` and `T3` do not have defaults
+ | Earlier TypeVar `T1` does
+31 | Generic[T1, T2, DefaultStrT, T3],
+32 | ): ...
+ |
+ ::: src/mdtest_snippet.py:3:1
+ |
+ 1 | from typing import TypeVar, Generic, Protocol
+ 2 |
+ 3 | T1 = TypeVar("T1", default=int)
+ | ------------------------------- `T1` defined here
+ 4 |
+ 5 | T2 = TypeVar("T2")
+ | ------------------ `T2` defined here
+ 6 | T3 = TypeVar("T3")
+ |
+info: rule `invalid-generic-class` is enabled by default
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_…_-_Regular_modules_(5c8e81664d1c7470).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_…_-_Regular_modules_(5c8e81664d1c7470).snap
index 21cc311cd8..257cc54b78 100644
--- a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_…_-_Regular_modules_(5c8e81664d1c7470).snap
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_…_-_Regular_modules_(5c8e81664d1c7470).snap
@@ -42,7 +42,11 @@ error[invalid-overload]: Overloads for function `func` must be followed by a non
9 | class Foo:
|
info: Attempting to call `func` will raise `TypeError` at runtime
-info: Overloaded functions without implementations are only permitted in stub files, on protocols, or for abstract methods
+info: Overloaded functions without implementations are only permitted:
+info: - in stub files
+info: - in `if TYPE_CHECKING` blocks
+info: - as methods on protocol classes
+info: - or as `@abstractmethod`-decorated methods on abstract classes
info: See https://docs.python.org/3/library/typing.html#typing.overload for more details
info: rule `invalid-overload` is enabled by default
@@ -58,7 +62,11 @@ error[invalid-overload]: Overloads for function `method` must be followed by a n
| ^^^^^^
|
info: Attempting to call `method` will raise `TypeError` at runtime
-info: Overloaded functions without implementations are only permitted in stub files, on protocols, or for abstract methods
+info: Overloaded functions without implementations are only permitted:
+info: - in stub files
+info: - in `if TYPE_CHECKING` blocks
+info: - as methods on protocol classes
+info: - or as `@abstractmethod`-decorated methods on abstract classes
info: See https://docs.python.org/3/library/typing.html#typing.overload for more details
info: rule `invalid-overload` is enabled by default
diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs
index a1319a3250..21dfb955a9 100644
--- a/crates/ty_python_semantic/src/place.rs
+++ b/crates/ty_python_semantic/src/place.rs
@@ -1,4 +1,5 @@
use ruff_db::files::File;
+use ruff_python_ast::PythonVersion;
use crate::dunder_all::dunder_all_names;
use crate::module_resolver::{KnownModule, file_to_module, resolve_module_confident};
@@ -1633,6 +1634,35 @@ mod implicit_globals {
}
}
+/// Looks up the type of an "implicit class body symbol". Returns [`Place::Undefined`] if
+/// `name` is not present as an implicit symbol in class bodies.
+///
+/// Implicit class body symbols are symbols such as `__qualname__`, `__module__`, `__doc__`,
+/// and `__firstlineno__` that Python implicitly makes available inside a class body during
+/// class creation.
+///
+/// See
+pub(crate) fn class_body_implicit_symbol<'db>(
+ db: &'db dyn Db,
+ name: &str,
+) -> PlaceAndQualifiers<'db> {
+ match name {
+ "__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
+ "__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
+ // __doc__ is `str` if there's a docstring, `None` if there isn't
+ "__doc__" => Place::bound(UnionType::from_elements(
+ db,
+ [KnownClass::Str.to_instance(db), Type::none(db)],
+ ))
+ .into(),
+ // __firstlineno__ was added in Python 3.13
+ "__firstlineno__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => {
+ Place::bound(KnownClass::Int.to_instance(db)).into()
+ }
+ _ => Place::Undefined.into(),
+ }
+}
+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum RequiresExplicitReExport {
Yes,
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index 726decfc07..b318638742 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -10691,7 +10691,6 @@ pub struct UnionTypeInstance<'db> {
/// ``. For `Union[int, str]`, this field is `None`, as we infer
/// the elements as type expressions. Use `value_expression_types` to get the
/// corresponding value expression types.
- #[expect(clippy::ref_option)]
#[returns(ref)]
_value_expr_types: Option]>>,
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 022e63fe33..61ee82e030 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -1815,13 +1815,6 @@ impl<'db> ClassLiteral<'db> {
})
}
- /// Determine if this is an abstract class.
- pub(super) fn is_abstract(self, db: &'db dyn Db) -> bool {
- self.metaclass(db)
- .as_class_literal()
- .is_some_and(|metaclass| metaclass.is_known(db, KnownClass::ABCMeta))
- }
-
/// Return the types of the decorators on this class
#[salsa::tracked(returns(deref), cycle_initial=decorators_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 3acd7b0a64..e136057d45 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -30,7 +30,7 @@ use crate::types::{
ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type,
protocol_class::ProtocolClass,
};
-use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy};
+use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance};
use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint};
use itertools::Itertools;
use ruff_db::{
@@ -894,15 +894,20 @@ declare_lint! {
///
/// ## Why is this bad?
/// There are several requirements that you must follow when defining a generic class.
+ /// Many of these result in `TypeError` being raised at runtime if they are violated.
///
/// ## Examples
/// ```python
- /// from typing import Generic, TypeVar
+ /// from typing_extensions import Generic, TypeVar
///
- /// T = TypeVar("T") # okay
+ /// T = TypeVar("T")
+ /// U = TypeVar("U", default=int)
///
/// # error: class uses both PEP-695 syntax and legacy syntax
/// class C[U](Generic[T]): ...
+ ///
+ /// # error: type parameter with default comes before type parameter without default
+ /// class D(Generic[U, T]): ...
/// ```
///
/// ## References
@@ -3695,6 +3700,90 @@ pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>(
}
}
+pub(crate) fn report_invalid_type_param_order<'db>(
+ context: &InferContext<'db, '_>,
+ class: ClassLiteral<'db>,
+ node: &ast::StmtClassDef,
+ typevar_with_default: TypeVarInstance<'db>,
+ invalid_later_typevars: &[TypeVarInstance<'db>],
+) {
+ let db = context.db();
+
+ let base_index = class
+ .explicit_bases(db)
+ .iter()
+ .position(|base| {
+ matches!(
+ base,
+ Type::KnownInstance(
+ KnownInstanceType::SubscriptedProtocol(_)
+ | KnownInstanceType::SubscriptedGeneric(_)
+ )
+ )
+ })
+ .expect(
+ "It should not be possible for a class to have a legacy generic context \
+ if it does not inherit from `Protocol[]` or `Generic[]`",
+ );
+
+ let base_node = &node.bases()[base_index];
+
+ let primary_diagnostic_range = base_node
+ .as_subscript_expr()
+ .map(|subscript| &*subscript.slice)
+ .unwrap_or(base_node)
+ .range();
+
+ let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, primary_diagnostic_range)
+ else {
+ return;
+ };
+
+ let mut diagnostic = builder.into_diagnostic(
+ "Type parameters without defaults cannot follow type parameters with defaults",
+ );
+
+ diagnostic.set_concise_message(format_args!(
+ "Type parameter `{}` without a default cannot follow earlier parameter `{}` with a default",
+ invalid_later_typevars[0].name(db),
+ typevar_with_default.name(db),
+ ));
+
+ if let [single_typevar] = invalid_later_typevars {
+ diagnostic.set_primary_message(format_args!(
+ "Type variable `{}` does not have a default",
+ single_typevar.name(db),
+ ));
+ } else {
+ let later_typevars =
+ format_enumeration(invalid_later_typevars.iter().map(|tv| tv.name(db)));
+ diagnostic.set_primary_message(format_args!(
+ "Type variables {later_typevars} do not have defaults",
+ ));
+ }
+
+ diagnostic.annotate(
+ Annotation::primary(Span::from(context.file()).with_range(primary_diagnostic_range))
+ .message(format_args!(
+ "Earlier TypeVar `{}` does",
+ typevar_with_default.name(db)
+ )),
+ );
+
+ for tvar in [typevar_with_default, invalid_later_typevars[0]] {
+ let Some(definition) = tvar.definition(db) else {
+ continue;
+ };
+ let file = definition.file(db);
+ diagnostic.annotate(
+ Annotation::secondary(Span::from(
+ definition.full_range(db, &parsed_module(db, file).load(db)),
+ ))
+ .message(format_args!("`{}` defined here", tvar.name(db))),
+ );
+ }
+}
+
pub(crate) fn report_rebound_typevar<'db>(
context: &InferContext<'db, '_>,
typevar_name: &ast::name::Name,
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 43940a4d88..d691b46cf9 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -28,9 +28,9 @@ use crate::module_resolver::{
use crate::node_key::NodeKey;
use crate::place::{
ConsideredDefinitions, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin,
- builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol,
- module_type_implicit_global_declaration, module_type_implicit_global_symbol, place,
- place_from_bindings, place_from_declarations, typing_extensions_symbol,
+ builtins_module_scope, builtins_symbol, class_body_implicit_symbol, explicit_global_symbol,
+ global_symbol, module_type_implicit_global_declaration, module_type_implicit_global_symbol,
+ place, place_from_bindings, place_from_declarations, typing_extensions_symbol,
};
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
@@ -78,7 +78,7 @@ use crate::types::diagnostic::{
report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type,
report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base,
report_invalid_return_type, report_invalid_type_checking_constant,
- report_named_tuple_field_with_leading_underscore,
+ report_invalid_type_param_order, report_named_tuple_field_with_leading_underscore,
report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
report_possibly_missing_attribute, report_possibly_unresolved_reference,
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
@@ -949,23 +949,62 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- let scope = class.body_scope(self.db()).scope(self.db());
- if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS)
- && let Some(parent) = scope.parent()
- {
- for self_typevar in class.typevars_referenced_in_definition(self.db()) {
- let self_typevar_name = self_typevar.typevar(self.db()).name(self.db());
- for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) {
- if let Some(other_typevar) =
- enclosing.binds_named_typevar(self.db(), self_typevar_name)
- {
- report_rebound_typevar(
- &self.context,
- self_typevar_name,
- class,
- class_node,
- other_typevar,
- );
+ if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS) {
+ if !class.has_pep_695_type_params(self.db())
+ && let Some(generic_context) = class.legacy_generic_context(self.db())
+ {
+ struct State<'db> {
+ typevar_with_default: TypeVarInstance<'db>,
+ invalid_later_tvars: Vec>,
+ }
+
+ let mut state: Option> = None;
+
+ for bound_typevar in generic_context.variables(self.db()) {
+ let typevar = bound_typevar.typevar(self.db());
+ let has_default = typevar.default_type(self.db()).is_some();
+
+ if let Some(state) = state.as_mut() {
+ if !has_default {
+ state.invalid_later_tvars.push(typevar);
+ }
+ } else if has_default {
+ state = Some(State {
+ typevar_with_default: typevar,
+ invalid_later_tvars: vec![],
+ });
+ }
+ }
+
+ if let Some(state) = state
+ && !state.invalid_later_tvars.is_empty()
+ {
+ report_invalid_type_param_order(
+ &self.context,
+ class,
+ class_node,
+ state.typevar_with_default,
+ &state.invalid_later_tvars,
+ );
+ }
+ }
+
+ let scope = class.body_scope(self.db()).scope(self.db());
+ if let Some(parent) = scope.parent() {
+ for self_typevar in class.typevars_referenced_in_definition(self.db()) {
+ let self_typevar_name = self_typevar.typevar(self.db()).name(self.db());
+ for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) {
+ if let Some(other_typevar) =
+ enclosing.binds_named_typevar(self.db(), self_typevar_name)
+ {
+ report_rebound_typevar(
+ &self.context,
+ self_typevar_name,
+ class,
+ class_node,
+ other_typevar,
+ );
+ }
}
}
}
@@ -1104,7 +1143,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if implementation.is_none() && !self.in_stub() {
let mut implementation_required = true;
- if let NodeWithScopeKind::Class(class_node_ref) = scope {
+ if function
+ .iter_overloads_and_implementation(self.db())
+ .all(|f| {
+ f.body_scope(self.db())
+ .scope(self.db())
+ .in_type_checking_block()
+ })
+ {
+ implementation_required = false;
+ } else if let NodeWithScopeKind::Class(class_node_ref) = scope {
let class = binding_type(
self.db(),
self.index
@@ -1113,7 +1161,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.expect_class_literal();
if class.is_protocol(self.db())
- || (class.is_abstract(self.db())
+ || (Type::ClassLiteral(class)
+ .is_subtype_of(self.db(), KnownClass::ABCMeta.to_instance(self.db()))
&& overloads.iter().all(|overload| {
overload.has_known_decorator(
self.db(),
@@ -1140,8 +1189,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
&function_node.name
));
diagnostic.info(
- "Overloaded functions without implementations are only permitted \
- in stub files, on protocols, or for abstract methods",
+ "Overloaded functions without implementations are only permitted:",
+ );
+ diagnostic.info(" - in stub files");
+ diagnostic.info(" - in `if TYPE_CHECKING` blocks");
+ diagnostic.info(" - as methods on protocol classes");
+ diagnostic.info(
+ " - or as `@abstractmethod`-decorated methods on abstract classes",
);
diagnostic.info(
"See https://docs.python.org/3/library/typing.html#typing.overload \
@@ -9210,6 +9264,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
PlaceAndQualifiers::from(Place::Undefined)
+ // If we're in a class body, check for implicit class body symbols first.
+ // These take precedence over globals.
+ .or_fall_back_to(db, || {
+ if scope.node(db).scope_kind().is_class()
+ && let Some(symbol) = place_expr.as_symbol()
+ {
+ let implicit = class_body_implicit_symbol(db, symbol.name());
+ if implicit.place.is_definitely_bound() {
+ return implicit.map_type(|ty| {
+ self.narrow_place_with_applicable_constraints(
+ place_expr,
+ ty,
+ &constraint_keys,
+ )
+ });
+ }
+ }
+ Place::Undefined.into()
+ })
// No nonlocal binding? Check the module's explicit globals.
// Avoid infinite recursion if `self.scope` already is the module's global scope.
.or_fall_back_to(db, || {
diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs
index 45c3f81de2..76fe3a35d4 100644
--- a/crates/ty_python_semantic/src/types/signatures.rs
+++ b/crates/ty_python_semantic/src/types/signatures.rs
@@ -1072,6 +1072,29 @@ impl<'db> Signature<'db> {
let mut check_types = |type1: Option>, type2: Option>| {
let type1 = type1.unwrap_or(Type::unknown());
let type2 = type2.unwrap_or(Type::unknown());
+
+ match (type1, type2) {
+ // This is a special case where the _same_ components of two different `ParamSpec`
+ // type variables are assignable to each other when they're both in an inferable
+ // position.
+ //
+ // `ParamSpec` type variables can only occur in parameter lists so this special case
+ // is present here instead of in `Type::has_relation_to_impl`.
+ (Type::TypeVar(typevar1), Type::TypeVar(typevar2))
+ if typevar1.paramspec_attr(db).is_some()
+ && typevar1.paramspec_attr(db) == typevar2.paramspec_attr(db)
+ && typevar1
+ .without_paramspec_attr(db)
+ .is_inferable(db, inferable)
+ && typevar2
+ .without_paramspec_attr(db)
+ .is_inferable(db, inferable) =>
+ {
+ return true;
+ }
+ _ => {}
+ }
+
!result
.intersect(
db,
diff --git a/dist-workspace.toml b/dist-workspace.toml
index 809beefa73..10bec6d868 100644
--- a/dist-workspace.toml
+++ b/dist-workspace.toml
@@ -66,7 +66,7 @@ install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
global = "depot-ubuntu-latest-4"
[dist.github-action-commits]
-"actions/checkout" = "1af3b93b6815bc44a9784bd300feb67ff0d1eeb3" # v6.0.0
+"actions/checkout" = "8e8c483db84b4bee98b60c0593521ed34d9990e8" # v6.0.1
"actions/upload-artifact" = "330a01c490aca151604b8cf639adc76d48f6c5d4" # v5.0.0
"actions/download-artifact" = "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" # v6.0.0
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index 1a35d66439..50b3f5d474 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,2 +1,2 @@
[toolchain]
-channel = "1.91"
+channel = "1.92"
diff --git a/ty.schema.json b/ty.schema.json
index 87feeb2507..74c142ec4c 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -595,7 +595,7 @@
},
"invalid-generic-class": {
"title": "detects invalid generic classes",
- "description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\n\n## Examples\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # okay\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)",
+ "description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\nMany of these result in `TypeError` being raised at runtime if they are violated.\n\n## Examples\n```python\nfrom typing_extensions import Generic, TypeVar\n\nT = TypeVar(\"T\")\nU = TypeVar(\"U\", default=int)\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n\n# error: type parameter with default comes before type parameter without default\nclass D(Generic[U, T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)",
"default": "error",
"oneOf": [
{