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
67 changes: 67 additions & 0 deletions crates/ty_ide/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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));
Expand Down
Loading