mirror of
https://github.com/astral-sh/ruff.git
synced 2025-12-23 09:19:39 +00:00
Special-case _replace in protocol checking
This commit is contained in:
parent
2c74fa4ba5
commit
b1c21256a9
2 changed files with 31 additions and 10 deletions
|
|
@ -359,10 +359,13 @@ def _(y: type[typing.NamedTuple]):
|
|||
def _(z: typing.NamedTuple[int]): ...
|
||||
```
|
||||
|
||||
Because `_replace` is synthesized with specific keyword-only parameters (to enable error detection
|
||||
for invalid keyword arguments), NamedTuple instances are not strictly assignable to
|
||||
`NamedTupleLike`. This is a trade-off: we gain compile-time detection of invalid `_replace`
|
||||
arguments at the cost of strict protocol compatibility.
|
||||
NamedTuples are assignable to `NamedTupleLike`, even though the synthesized `_replace` signature
|
||||
`(*, x: int = ..., y: int = ...)` is not strictly a subtype of `(**kwargs: Any)`. We special-case
|
||||
this in protocol checking because:
|
||||
|
||||
1. The specific `_replace` signature enables compile-time detection of invalid keyword arguments
|
||||
1. In practice, all valid calls to `_replace` on a NamedTupleLike will also work on any concrete
|
||||
NamedTuple
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, Protocol, Iterable, Any
|
||||
|
|
@ -376,16 +379,13 @@ reveal_type(Point._make) # revealed: bound method <class 'Point'>._make(iterabl
|
|||
reveal_type(Point._asdict) # revealed: def _asdict(self) -> dict[str, Any]
|
||||
reveal_type(Point._replace) # revealed: (self: Point, *, x: int = ..., y: int = ...) -> Point
|
||||
|
||||
# Point is not assignable to NamedTuple because the synthesized `_replace` signature
|
||||
# `(*, x: int = ..., y: int = ...)` is not a subtype of `(**kwargs: Any)`. This is a
|
||||
# deliberate trade-off: detecting invalid `_replace` arguments at the cost of protocol
|
||||
# compatibility. Mypy makes the same trade-off.
|
||||
# error: [static-assert-error]
|
||||
# Point is assignable to NamedTuple (we special-case _replace in protocol checking).
|
||||
static_assert(is_assignable_to(Point, NamedTuple))
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `expects_named_tuple` is incorrect: Expected `tuple[object, ...] & NamedTupleLike`, found `Point`"
|
||||
# NamedTuple instances can be passed to functions expecting NamedTupleLike.
|
||||
expects_named_tuple(Point(x=42, y=56))
|
||||
|
||||
# But plain tuples are not NamedTupleLike (they don't have _make, _asdict, _replace, etc.).
|
||||
# error: [invalid-argument-type] "Argument to function `expects_named_tuple` is incorrect: Expected `tuple[object, ...] & NamedTupleLike`, found `tuple[Literal[1], Literal[2]]`"
|
||||
expects_named_tuple((1, 2))
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use std::borrow::Cow;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use super::class::CodeGeneratorKind;
|
||||
use super::protocol_class::ProtocolInterface;
|
||||
use super::{BoundTypeVarInstance, ClassType, KnownClass, SubclassOfType, Type, TypeVarVariance};
|
||||
use crate::place::PlaceAndQualifiers;
|
||||
|
|
@ -132,6 +133,16 @@ impl<'db> Type<'db> {
|
|||
relation_visitor: &HasRelationToVisitor<'db>,
|
||||
disjointness_visitor: &IsDisjointVisitor<'db>,
|
||||
) -> ConstraintSet<'db> {
|
||||
// Iff we're checking a NamedTuple against NamedTupleLike, skip the `_replace` method
|
||||
// signature. NamedTuples synthesize `_replace` methods with specific keyword-only
|
||||
// parameters (to detect invalid arguments), which are not strictly subtypes of the
|
||||
// protocol's `(**kwargs)` signature, but are intended to be considered as satisfying it.
|
||||
let is_namedtuple_protocol_check = matches!(&protocol.inner, Protocol::FromClass(class) if class.is_known(db, KnownClass::NamedTupleLike))
|
||||
&& self.as_nominal_instance().is_some_and(|instance| {
|
||||
let (class_literal, specialization) = instance.class(db).class_literal(db);
|
||||
CodeGeneratorKind::NamedTuple.matches(db, class_literal, specialization)
|
||||
});
|
||||
|
||||
let structurally_satisfied = if let Type::ProtocolInstance(self_protocol) = self {
|
||||
self_protocol.interface(db).has_relation_to_impl(
|
||||
db,
|
||||
|
|
@ -146,6 +157,16 @@ impl<'db> Type<'db> {
|
|||
.inner
|
||||
.interface(db)
|
||||
.members(db)
|
||||
.filter(|member| {
|
||||
// Skip `_replace` check for NamedTuple vs NamedTupleLike. NamedTuples
|
||||
// synthesize `_replace` with specific keyword-only parameters to detect
|
||||
// invalid arguments, but this signature is not a strict subtype of the
|
||||
// protocol's `(**kwargs)` signature.
|
||||
if is_namedtuple_protocol_check && member.name() == "_replace" {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.when_all(db, |member| {
|
||||
member.is_satisfied_by(
|
||||
db,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue