Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions crates/ty_python_semantic/resources/mdtest/named_tuple.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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
Expand All @@ -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`
Expand Down
16 changes: 14 additions & 2 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
Expand Down
Loading