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]
|
# error: [invalid-argument-type]
|
||||||
Person(id="3", name="Eve")
|
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
|
alice.id = 42
|
||||||
|
# error: [invalid-assignment]
|
||||||
bob.age = None
|
bob.age = None
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -151,9 +159,42 @@ from typing import NamedTuple
|
||||||
class User(NamedTuple):
|
class User(NamedTuple):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
age: int | None
|
||||||
|
nickname: str
|
||||||
|
|
||||||
class SuperUser(User):
|
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
|
### Generic named tuples
|
||||||
|
@ -164,13 +205,29 @@ python-version = "3.12"
|
||||||
```
|
```
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple, Generic, TypeVar
|
||||||
|
|
||||||
class Property[T](NamedTuple):
|
class Property[T](NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
value: T
|
value: T
|
||||||
|
|
||||||
reveal_type(Property("height", 3.4)) # revealed: Property[float]
|
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`
|
## Attributes on `NamedTuple`
|
||||||
|
|
|
@ -24,8 +24,8 @@ use crate::types::tuple::{TupleSpec, TupleType};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
ApplyTypeMappingVisitor, BareTypeAliasType, Binding, BoundSuperError, BoundSuperType,
|
ApplyTypeMappingVisitor, BareTypeAliasType, Binding, BoundSuperError, BoundSuperType,
|
||||||
CallableType, DataclassParams, DeprecatedInstance, HasRelationToVisitor, KnownInstanceType,
|
CallableType, DataclassParams, DeprecatedInstance, HasRelationToVisitor, KnownInstanceType,
|
||||||
NormalizedVisitor, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation,
|
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
|
||||||
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
|
TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
|
||||||
infer_definition_types, todo_type,
|
infer_definition_types, todo_type,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -1862,6 +1862,18 @@ impl<'db> ClassLiteral<'db> {
|
||||||
.with_qualifiers(TypeQualifiers::CLASS_VAR);
|
.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 body_scope = self.body_scope(db);
|
||||||
let symbol = class_symbol(db, body_scope, name).map_type(|ty| {
|
let symbol = class_symbol(db, body_scope, name).map_type(|ty| {
|
||||||
// The `__new__` and `__init__` members of a non-specialized generic class are handled
|
// The `__new__` and `__init__` members of a non-specialized generic class are handled
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue