mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[ty] Fix the inferred MRO of functional namedtuple classes (#22722)
This commit is contained in:
@@ -8,7 +8,7 @@ name, and not just by its numeric position within the tuple:
|
||||
### Basics
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from typing import NamedTuple, Sequence
|
||||
from ty_extensions import static_assert, is_subtype_of, is_assignable_to, reveal_mro
|
||||
|
||||
class Person(NamedTuple):
|
||||
@@ -30,6 +30,8 @@ reveal_mro(Person)
|
||||
|
||||
static_assert(is_subtype_of(Person, tuple[int, str, int | None]))
|
||||
static_assert(is_subtype_of(Person, tuple[object, ...]))
|
||||
static_assert(is_subtype_of(Person, Sequence[int | str | None]))
|
||||
static_assert(is_subtype_of(Person, Sequence[object]))
|
||||
static_assert(not is_assignable_to(Person, tuple[int, str, int]))
|
||||
static_assert(not is_assignable_to(Person, tuple[int, str]))
|
||||
|
||||
@@ -166,7 +168,7 @@ reveal_type(container.items) # revealed: list[int]
|
||||
reveal_type(container.mapping) # revealed: dict[str, bool]
|
||||
|
||||
# MRO includes the properly specialized tuple type.
|
||||
# revealed: (<class 'Url'>, <class 'tuple[str, int]'>, <class 'object'>)
|
||||
# revealed: (<class 'Url'>, <class 'tuple[str, int]'>, <class 'Sequence[str | int]'>, <class 'Reversible[str | int]'>, <class 'Collection[str | int]'>, <class 'Iterable[str | int]'>, <class 'Container[str | int]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Url)
|
||||
|
||||
static_assert(is_subtype_of(Url, tuple[str, int]))
|
||||
@@ -219,7 +221,8 @@ NT = NamedTuple("NT", fields)
|
||||
|
||||
# Fields are unknown, so attribute access returns Any and MRO has Unknown tuple.
|
||||
reveal_type(NT) # revealed: <class 'NT'>
|
||||
reveal_mro(NT) # revealed: (<class 'NT'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
|
||||
# revealed: (<class 'NT'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(NT)
|
||||
reveal_type(NT(1, "a").x) # revealed: Any
|
||||
```
|
||||
|
||||
@@ -237,7 +240,8 @@ NT = collections.namedtuple("NT", field_names)
|
||||
|
||||
# Fields are unknown, so attribute access returns Any and MRO has Unknown tuple.
|
||||
reveal_type(NT) # revealed: <class 'NT'>
|
||||
reveal_mro(NT) # revealed: (<class 'NT'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
|
||||
# revealed: (<class 'NT'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(NT)
|
||||
reveal_type(NT(1, 2).x) # revealed: Any
|
||||
```
|
||||
|
||||
@@ -254,7 +258,7 @@ class Url(NamedTuple("Url", [("host", str), ("path", str)])):
|
||||
pass
|
||||
|
||||
reveal_type(Url) # revealed: <class 'Url'>
|
||||
# revealed: (<class 'mdtest_snippet.Url @ src/mdtest_snippet.py:4:7'>, <class 'mdtest_snippet.Url @ src/mdtest_snippet.py:4:11'>, <class 'tuple[str, str]'>, <class 'object'>)
|
||||
# revealed: (<class 'mdtest_snippet.Url @ src/mdtest_snippet.py:4:7'>, <class 'mdtest_snippet.Url @ src/mdtest_snippet.py:4:11'>, <class 'tuple[str, str]'>, <class 'Sequence[str]'>, <class 'Reversible[str]'>, <class 'Collection[str]'>, <class 'Iterable[str]'>, <class 'Container[str]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Url)
|
||||
reveal_type(Url.__new__) # revealed: [Self](cls: type[Self], host: str, path: str) -> Self
|
||||
|
||||
@@ -364,7 +368,7 @@ reveal_type(config) # revealed: GroundTruth
|
||||
reveal_type(config.duration) # revealed: Any
|
||||
|
||||
# Namedtuples with unknown fields inherit from tuple[Unknown, ...] to avoid false positives.
|
||||
# revealed: (<class 'GroundTruth'>, <class 'tuple[Unknown, ...]'>, <class 'object'>)
|
||||
# revealed: (<class 'GroundTruth'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(GroundTruth)
|
||||
|
||||
# No index-out-of-bounds error since the tuple length is unknown.
|
||||
@@ -383,33 +387,38 @@ from ty_extensions import reveal_mro
|
||||
# String field names (space-separated)
|
||||
Point1 = collections.namedtuple("Point", "x y")
|
||||
reveal_type(Point1) # revealed: <class 'Point'>
|
||||
reveal_mro(Point1) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Point1)
|
||||
|
||||
# String field names with multiple spaces
|
||||
Point1a = collections.namedtuple("Point", "x y")
|
||||
reveal_type(Point1a) # revealed: <class 'Point'>
|
||||
reveal_mro(Point1a) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Point1a)
|
||||
|
||||
# String field names (comma-separated also works at runtime)
|
||||
Point2 = collections.namedtuple("Point", "x, y")
|
||||
reveal_type(Point2) # revealed: <class 'Point'>
|
||||
reveal_mro(Point2) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Point2)
|
||||
|
||||
# List of strings
|
||||
Point3 = collections.namedtuple("Point", ["x", "y"])
|
||||
reveal_type(Point3) # revealed: <class 'Point'>
|
||||
reveal_mro(Point3) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Point3)
|
||||
|
||||
# Tuple of strings
|
||||
Point4 = collections.namedtuple("Point", ("x", "y"))
|
||||
reveal_type(Point4) # revealed: <class 'Point'>
|
||||
reveal_mro(Point4) # revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
|
||||
# revealed: (<class 'Point'>, <class 'tuple[Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Point4)
|
||||
# Invalid: integer is not a valid typename
|
||||
# error: [invalid-argument-type]
|
||||
Invalid = collections.namedtuple(123, ["x", "y"])
|
||||
reveal_type(Invalid) # revealed: <class '<unknown>'>
|
||||
reveal_mro(Invalid) # revealed: (<class '<unknown>'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class '<unknown>'>, <class 'tuple[Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Invalid)
|
||||
|
||||
# Invalid: too many positional arguments
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `namedtuple`: expected 2, got 4"
|
||||
@@ -452,7 +461,8 @@ from ty_extensions import reveal_mro
|
||||
# Both `typename` and `field_names` can be passed as keyword arguments
|
||||
NT1 = collections.namedtuple(typename="NT1", field_names="x y")
|
||||
reveal_type(NT1) # revealed: <class 'NT1'>
|
||||
reveal_mro(NT1) # revealed: (<class 'NT1'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'NT1'>, <class 'tuple[Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(NT1)
|
||||
|
||||
nt1 = NT1(1, 2)
|
||||
reveal_type(nt1.x) # revealed: Any
|
||||
@@ -461,7 +471,8 @@ reveal_type(nt1.y) # revealed: Any
|
||||
# Only `field_names` as keyword argument
|
||||
NT2 = collections.namedtuple("NT2", field_names=["a", "b", "c"])
|
||||
reveal_type(NT2) # revealed: <class 'NT2'>
|
||||
reveal_mro(NT2) # revealed: (<class 'NT2'>, <class 'tuple[Any, Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'NT2'>, <class 'tuple[Any, Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(NT2)
|
||||
|
||||
nt2 = NT2(1, 2, 3)
|
||||
reveal_type(nt2.a) # revealed: Any
|
||||
@@ -494,7 +505,8 @@ from ty_extensions import reveal_mro
|
||||
Point = collections.namedtuple("Point", ["x", "class", "_y", "z", "z"], rename=True)
|
||||
reveal_type(Point) # revealed: <class 'Point'>
|
||||
reveal_type(Point.__new__) # revealed: [Self](cls: type[Self], x: Any, _1: Any, _2: Any, z: Any, _4: Any) -> Self
|
||||
reveal_mro(Point) # revealed: (<class 'Point'>, <class 'tuple[Any, Any, Any, Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'Point'>, <class 'tuple[Any, Any, Any, Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Point)
|
||||
p = Point(1, 2, 3, 4, 5)
|
||||
reveal_type(p.x) # revealed: Any
|
||||
reveal_type(p._1) # revealed: Any
|
||||
@@ -534,7 +546,8 @@ Person = collections.namedtuple("Person", ["name", "age", "city"], defaults=["Un
|
||||
reveal_type(Person) # revealed: <class 'Person'>
|
||||
reveal_type(Person.__new__) # revealed: [Self](cls: type[Self], name: Any, age: Any, city: Any = "Unknown") -> Self
|
||||
|
||||
reveal_mro(Person) # revealed: (<class 'Person'>, <class 'tuple[Any, Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'Person'>, <class 'tuple[Any, Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Person)
|
||||
# Can create with all fields
|
||||
person1 = Person("Alice", 30, "NYC")
|
||||
# Can omit the field with default
|
||||
@@ -556,14 +569,16 @@ reveal_type(TooManyDefaults.__new__) # revealed: [Self](cls: type[Self], x: Any
|
||||
# error: [unknown-argument]
|
||||
Bad1 = collections.namedtuple("Bad1", ["x", "y"], foobarbaz=42)
|
||||
reveal_type(Bad1) # revealed: <class 'Bad1'>
|
||||
reveal_mro(Bad1) # revealed: (<class 'Bad1'>, <class 'tuple[Any, Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'Bad1'>, <class 'tuple[Any, Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Bad1)
|
||||
|
||||
# Multiple unknown keyword arguments
|
||||
# error: [unknown-argument]
|
||||
# error: [unknown-argument]
|
||||
Bad2 = collections.namedtuple("Bad2", ["x"], invalid1=True, invalid2=False)
|
||||
reveal_type(Bad2) # revealed: <class 'Bad2'>
|
||||
reveal_mro(Bad2) # revealed: (<class 'Bad2'>, <class 'tuple[Any]'>, <class 'object'>)
|
||||
# revealed: (<class 'Bad2'>, <class 'tuple[Any]'>, <class 'Sequence[Any]'>, <class 'Reversible[Any]'>, <class 'Collection[Any]'>, <class 'Iterable[Any]'>, <class 'Container[Any]'>, typing.Protocol, typing.Generic, <class 'object'>)
|
||||
reveal_mro(Bad2)
|
||||
|
||||
# Invalid type for `defaults` (not Iterable[Any] | None)
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter `defaults` of `namedtuple()`"
|
||||
|
||||
@@ -5497,23 +5497,26 @@ impl<'db> DynamicNamedTupleLiteral<'db> {
|
||||
|
||||
/// Compute the MRO for this namedtuple.
|
||||
///
|
||||
/// The MRO is `[self, tuple[field_types...], object]`.
|
||||
/// For example, `namedtuple("Point", [("x", int), ("y", int)])` has MRO
|
||||
/// `[Point, tuple[int, int], object]`.
|
||||
/// The MRO is the MRO of the class's tuple base class, prepended by `self`.
|
||||
/// For example, `namedtuple("Point", [("x", int), ("y", int)])` has the following MRO:
|
||||
///
|
||||
/// 1. `<class 'Point'>`
|
||||
/// 2. `<class 'tuple[int, int]'>`
|
||||
/// 3. `<class 'Sequence[int]'>`
|
||||
/// 4. `<class 'Reversible[int]'>`
|
||||
/// 5. `<class 'Collection[int]'>`
|
||||
/// 6. `<class 'Iterable[int]'>`
|
||||
/// 7. `<class 'Container[int]'>`
|
||||
/// 8. `typing.Protocol`
|
||||
/// 9. `typing.Generic`
|
||||
/// 10. `<class 'object'>`
|
||||
#[salsa::tracked(returns(ref), heap_size = ruff_memory_usage::heap_size)]
|
||||
pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> {
|
||||
let self_base = ClassBase::Class(ClassType::NonGeneric(self.into()));
|
||||
let tuple_class = self.tuple_base_class(db);
|
||||
let object_class = KnownClass::Object
|
||||
.to_class_literal(db)
|
||||
.as_class_literal()
|
||||
.expect("object should be a class literal")
|
||||
.default_specialization(db);
|
||||
Mro::from([
|
||||
self_base,
|
||||
ClassBase::Class(tuple_class),
|
||||
ClassBase::Class(object_class),
|
||||
])
|
||||
std::iter::once(self_base)
|
||||
.chain(tuple_class.iter_mro(db))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the metaclass of this dynamic namedtuple.
|
||||
|
||||
Reference in New Issue
Block a user