mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 19:41:34 +00:00
[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
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:
parent
5f501374c4
commit
8599c7e5b3
5 changed files with 179 additions and 8 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue