[ty] Fixup a few details around version-specific dataclass features (#21453)
Some checks are pending
CI / mkdocs (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Alex Waygood 2025-11-14 15:04:55 +00:00 committed by GitHub
parent 5f501374c4
commit 8599c7e5b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 179 additions and 8 deletions

View file

@ -2411,6 +2411,73 @@ Answer.<CURSOR>
);
}
#[test]
fn namedtuple_methods() {
let builder = completion_test_builder(
"\
from typing import NamedTuple
class Quux(NamedTuple):
x: int
y: str
quux = Quux()
quux.<CURSOR>
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
count :: bound method Quux.count(value: Any, /) -> int
index :: bound method Quux.index(value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int
x :: int
y :: str
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], (value: tuple[_T@__add__, ...], /) -> tuple[int | str | _T@__add__, ...]]
__annotations__ :: dict[str, Any]
__class__ :: type[Quux]
__class_getitem__ :: bound method type[Quux].__class_getitem__(item: Any, /) -> GenericAlias
__contains__ :: bound method Quux.__contains__(key: object, /) -> bool
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
__dict__ :: dict[str, Any]
__dir__ :: bound method Quux.__dir__() -> Iterable[str]
__doc__ :: str | None
__eq__ :: bound method Quux.__eq__(value: object, /) -> bool
__format__ :: bound method Quux.__format__(format_spec: str, /) -> str
__ge__ :: bound method Quux.__ge__(value: tuple[int | str, ...], /) -> bool
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
__getitem__ :: Overload[(index: Literal[-2, 0], /) -> int, (index: Literal[-1, 1], /) -> str, (index: SupportsIndex, /) -> int | str, (index: slice[Any, Any, Any], /) -> tuple[int | str, ...]]
__getstate__ :: bound method Quux.__getstate__() -> object
__gt__ :: bound method Quux.__gt__(value: tuple[int | str, ...], /) -> bool
__hash__ :: bound method Quux.__hash__() -> int
__init__ :: bound method Quux.__init__() -> None
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
__iter__ :: bound method Quux.__iter__() -> Iterator[int | str]
__le__ :: bound method Quux.__le__(value: tuple[int | str, ...], /) -> bool
__len__ :: () -> Literal[2]
__lt__ :: bound method Quux.__lt__(value: tuple[int | str, ...], /) -> bool
__module__ :: str
__mul__ :: bound method Quux.__mul__(value: SupportsIndex, /) -> tuple[int | str, ...]
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: (x: int, y: str) -> None
__orig_bases__ :: tuple[Any, ...]
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__replace__ :: bound method NamedTupleFallback.__replace__(**kwargs: Any) -> NamedTupleFallback
__repr__ :: bound method Quux.__repr__() -> str
__reversed__ :: bound method Quux.__reversed__() -> Iterator[int | str]
__rmul__ :: bound method Quux.__rmul__(value: SupportsIndex, /) -> tuple[int | str, ...]
__setattr__ :: bound method Quux.__setattr__(name: str, value: Any, /) -> None
__sizeof__ :: bound method Quux.__sizeof__() -> int
__str__ :: bound method Quux.__str__() -> str
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
_asdict :: bound method NamedTupleFallback._asdict() -> dict[str, Any]
_field_defaults :: dict[str, Any]
_fields :: tuple[str, ...]
_make :: bound method type[NamedTupleFallback]._make(iterable: Iterable[Any]) -> NamedTupleFallback
_replace :: bound method NamedTupleFallback._replace(**kwargs: Any) -> NamedTupleFallback
");
}
// We don't yet take function parameters into account.
#[test]
fn call_prefix1() {

View file

@ -667,8 +667,13 @@ reveal_type(B.__slots__) # revealed: tuple[Literal["x"], Literal["y"]]
### `weakref_slot`
When a dataclass is defined with `weakref_slot=True`, the `__weakref__` attribute is generated. For
now, we do not attempt to infer a more precise type for it.
When a dataclass is defined with `weakref_slot=True` on Python >=3.11, the `__weakref__` attribute
is generated. For now, we do not attempt to infer a more precise type for it.
```toml
[environment]
python-version = "3.11"
```
```py
from dataclasses import dataclass
@ -680,6 +685,58 @@ class C:
reveal_type(C.__weakref__) # revealed: Any | None
```
The `__weakref__` attribute is correctly not modeled as existing on instances of slotted dataclasses
where the class definition was not marked with `weakref=True`:
```py
from dataclasses import dataclass
@dataclass(slots=True)
class C: ...
# error: [unresolved-attribute]
reveal_type(C().__weakref__) # revealed: Unknown
```
### New features are not available on old Python versions
Certain parameters to `@dataclass` were added on newer Python versions; we do not infer them as
having any effect on older Python versions:
```toml
[environment]
python-version = "3.9"
```
```py
from dataclasses import dataclass
# fmt: off
# TODO: these nonexistent keyword arguments should cause us to emit diagnostics on Python 3.9
@dataclass(
slots=True,
weakref_slot=True,
match_args=True
)
class Foo: ...
# fmt: on
# error: [unresolved-attribute]
reveal_type(Foo.__slots__) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(Foo.__match_args__) # revealed: Unknown
# TODO: this actually *does* exist at runtime
# (all classes and non-slotted instances have it available by default).
# We could try to model that more fully...?
# It's not added by the dataclasses machinery, though.
#
# error: [unresolved-attribute]
reveal_type(Foo.__weakref__) # revealed: Unknown
```
## `Final` fields
Dataclass fields can be annotated with `Final`, which means that the field cannot be reassigned

View file

@ -682,7 +682,12 @@ static_assert(has_member(C(1), "__slots__"))
#### `weakref_slot=True`
When `weakref_slot=True`, the corresponding dunder attribute becomes available:
When `weakref_slot=True` on Python >=3.11, the corresponding dunder attribute becomes available:
```toml
[environment]
python-version = "3.11"
```
```py
from ty_extensions import has_member, static_assert
@ -742,6 +747,32 @@ def _(c: C):
static_assert(has_member(c, "__match_args__"))
```
### Attributes added on new Python versions are not synthesized on older Python versions
```toml
[environment]
python-version = "3.9"
```
```py
from dataclasses import dataclass
from ty_extensions import static_assert, has_member
# TODO: these parameters don't exist on Python 3.9;
# we should emit a diagnostic (or two)
@dataclass(slots=True, weakref_slot=True)
class F: ...
static_assert(not has_member(F, "__slots__"))
static_assert(not has_member(F, "__match_args__"))
# In actual fact, all non-slotted instances have this attribute
# (and even slotted instances can, if `__weakref__` is included in `__slots__`);
# we could possibly model that more fully?
# It's not added by the dataclasses machinery, though
static_assert(not has_member(F(), "__weakref__"))
```
### Attributes not available at runtime
Typeshed includes some attributes in `object` that are not available for some (builtin) types. For

View file

@ -993,7 +993,11 @@ impl<'db> Bindings<'db> {
flags |= DataclassFlags::FROZEN;
}
if to_bool(match_args, true) {
flags |= DataclassFlags::MATCH_ARGS;
if Program::get(db).python_version(db) >= PythonVersion::PY310 {
flags |= DataclassFlags::MATCH_ARGS;
} else {
// TODO: emit diagnostic
}
}
if to_bool(kw_only, false) {
if Program::get(db).python_version(db) >= PythonVersion::PY310 {
@ -1003,10 +1007,18 @@ impl<'db> Bindings<'db> {
}
}
if to_bool(slots, false) {
flags |= DataclassFlags::SLOTS;
if Program::get(db).python_version(db) >= PythonVersion::PY310 {
flags |= DataclassFlags::SLOTS;
} else {
// TODO: emit diagnostic
}
}
if to_bool(weakref_slot, false) {
flags |= DataclassFlags::WEAKREF_SLOT;
if Program::get(db).python_version(db) >= PythonVersion::PY311 {
flags |= DataclassFlags::WEAKREF_SLOT;
} else {
// TODO: emit diagnostic
}
}
let params = DataclassParams::from_flags(db, flags);

View file

@ -2397,7 +2397,9 @@ impl<'db> ClassLiteral<'db> {
.map(|(name, _)| Type::string_literal(db, name));
Some(Type::heterogeneous_tuple(db, match_args))
}
(CodeGeneratorKind::DataclassLike(_), "__weakref__") => {
(CodeGeneratorKind::DataclassLike(_), "__weakref__")
if Program::get(db).python_version(db) >= PythonVersion::PY311 =>
{
if !has_dataclass_param(DataclassFlags::WEAKREF_SLOT)
|| !has_dataclass_param(DataclassFlags::SLOTS)
{
@ -2456,7 +2458,9 @@ impl<'db> ClassLiteral<'db> {
}
None
}
(CodeGeneratorKind::DataclassLike(_), "__slots__") => {
(CodeGeneratorKind::DataclassLike(_), "__slots__")
if Program::get(db).python_version(db) >= PythonVersion::PY310 =>
{
has_dataclass_param(DataclassFlags::SLOTS).then(|| {
let fields = self.fields(db, specialization, field_policy);
let slots = fields.keys().map(|name| Type::string_literal(db, name));