diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 8c90597c74..d0660893da 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -2411,6 +2411,73 @@ Answer. ); } + #[test] + fn namedtuple_methods() { + let builder = completion_test_builder( + "\ +from typing import NamedTuple + +class Quux(NamedTuple): + x: int + y: str + +quux = Quux() +quux. +", + ); + + 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() { diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 906e79c7b9..0950cc33c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index 0ef6eda4a0..38404c0457 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -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 diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 38fcc588a4..e74eaf7560 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -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); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 809c5e7259..0232b00e76 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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));