mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Synthesize read-only properties for all declared members on NamedTuple
classes (#19899)
This commit is contained in:
parent
82350a398e
commit
f6093452ed
2 changed files with 74 additions and 5 deletions
|
@ -74,8 +74,16 @@ Person(3, "Eve", 99, "extra")
|
|||
# error: [invalid-argument-type]
|
||||
Person(id="3", name="Eve")
|
||||
|
||||
# TODO: over-writing NamedTuple fields should be an error
|
||||
reveal_type(Person.id) # revealed: property
|
||||
reveal_type(Person.name) # revealed: property
|
||||
reveal_type(Person.age) # revealed: property
|
||||
|
||||
# TODO... the error is correct, but this is not the friendliest error message
|
||||
# for assigning to a read-only property :-)
|
||||
#
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `id` on type `Person` with custom `__set__` method"
|
||||
alice.id = 42
|
||||
# error: [invalid-assignment]
|
||||
bob.age = None
|
||||
```
|
||||
|
||||
|
@ -151,9 +159,42 @@ from typing import NamedTuple
|
|||
class User(NamedTuple):
|
||||
id: int
|
||||
name: str
|
||||
age: int | None
|
||||
nickname: str
|
||||
|
||||
class SuperUser(User):
|
||||
id: int # this should be an error
|
||||
# TODO: this should be an error because it implies that the `id` attribute on
|
||||
# `SuperUser` is mutable, but the read-only `id` property from the superclass
|
||||
# has not been overridden in the class body
|
||||
id: int
|
||||
|
||||
# this is fine; overriding a read-only attribute with a mutable one
|
||||
# does not conflict with the Liskov Substitution Principle
|
||||
name: str = "foo"
|
||||
|
||||
# this is also fine
|
||||
@property
|
||||
def age(self) -> int:
|
||||
return super().age or 42
|
||||
|
||||
def now_called_robert(self):
|
||||
self.name = "Robert" # fine because overridden with a mutable attribute
|
||||
|
||||
# TODO: this should cause us to emit an error as we're assigning to a read-only property
|
||||
# inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159)
|
||||
self.nickname = "Bob"
|
||||
|
||||
james = SuperUser(0, "James", 42, "Jimmy")
|
||||
|
||||
# fine because the property on the superclass was overridden with a mutable attribute
|
||||
# on the subclass
|
||||
james.name = "Robert"
|
||||
|
||||
# TODO: the error is correct (can't assign to the read-only property inherited from the superclass)
|
||||
# but the error message could be friendlier :-)
|
||||
#
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `nickname` on type `SuperUser` with custom `__set__` method"
|
||||
james.nickname = "Bob"
|
||||
```
|
||||
|
||||
### Generic named tuples
|
||||
|
@ -164,13 +205,29 @@ python-version = "3.12"
|
|||
```
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from typing import NamedTuple, Generic, TypeVar
|
||||
|
||||
class Property[T](NamedTuple):
|
||||
name: str
|
||||
value: T
|
||||
|
||||
reveal_type(Property("height", 3.4)) # revealed: Property[float]
|
||||
reveal_type(Property.value) # revealed: property
|
||||
reveal_type(Property.value.fget) # revealed: (self, /) -> Unknown
|
||||
reveal_type(Property[str].value.fget) # revealed: (self, /) -> str
|
||||
reveal_type(Property("height", 3.4).value) # revealed: float
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class LegacyProperty(NamedTuple, Generic[T]):
|
||||
name: str
|
||||
value: T
|
||||
|
||||
reveal_type(LegacyProperty("height", 42)) # revealed: LegacyProperty[int]
|
||||
reveal_type(LegacyProperty.value) # revealed: property
|
||||
reveal_type(LegacyProperty.value.fget) # revealed: (self, /) -> Unknown
|
||||
reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str
|
||||
reveal_type(LegacyProperty("height", 3.4).value) # revealed: float
|
||||
```
|
||||
|
||||
## Attributes on `NamedTuple`
|
||||
|
|
|
@ -24,8 +24,8 @@ use crate::types::tuple::{TupleSpec, TupleType};
|
|||
use crate::types::{
|
||||
ApplyTypeMappingVisitor, BareTypeAliasType, Binding, BoundSuperError, BoundSuperType,
|
||||
CallableType, DataclassParams, DeprecatedInstance, HasRelationToVisitor, KnownInstanceType,
|
||||
NormalizedVisitor, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
|
||||
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
|
||||
TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
|
||||
infer_definition_types, todo_type,
|
||||
};
|
||||
use crate::{
|
||||
|
@ -1862,6 +1862,18 @@ impl<'db> ClassLiteral<'db> {
|
|||
.with_qualifiers(TypeQualifiers::CLASS_VAR);
|
||||
}
|
||||
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, self) {
|
||||
if let Some(field) = self.own_fields(db, specialization).get(name) {
|
||||
let property_getter_signature = Signature::new(
|
||||
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]),
|
||||
Some(field.declared_ty),
|
||||
);
|
||||
let property_getter = CallableType::single(db, property_getter_signature);
|
||||
let property = PropertyInstanceType::new(db, Some(property_getter), None);
|
||||
return Place::bound(Type::PropertyInstance(property)).into();
|
||||
}
|
||||
}
|
||||
|
||||
let body_scope = self.body_scope(db);
|
||||
let symbol = class_symbol(db, body_scope, name).map_type(|ty| {
|
||||
// The `__new__` and `__init__` members of a non-specialized generic class are handled
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue