mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 13:30:49 -05:00
[ty] Synthesize __delitem__ for TypedDict to allow deleting non-required keys (#22122)
## Summary TypedDict now synthesizes a proper `__delitem__` method that... - ...allows deletion of `NotRequired` keys and keys in `total=False` TypedDicts. - ...rejects deletion of required keys (synthesizes `__delitem__(k: Never)`).
This commit is contained in:
@@ -239,21 +239,43 @@ del g[0]
|
||||
### TypedDict deletion
|
||||
|
||||
Deleting a required key from a TypedDict is a type error because it would make the object no longer
|
||||
a valid instance of that TypedDict type.
|
||||
a valid instance of that TypedDict type. However, deleting `NotRequired` keys (or keys in
|
||||
`total=False` TypedDicts) is allowed.
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict
|
||||
from typing_extensions import TypedDict, NotRequired
|
||||
|
||||
class Movie(TypedDict):
|
||||
name: str
|
||||
year: int
|
||||
|
||||
m: Movie = {"name": "Blade Runner", "year": 1982}
|
||||
class PartialMovie(TypedDict, total=False):
|
||||
name: str
|
||||
year: int
|
||||
|
||||
class MixedMovie(TypedDict):
|
||||
name: str
|
||||
year: NotRequired[int]
|
||||
|
||||
m: Movie = {"name": "Blade Runner", "year": 1982}
|
||||
p: PartialMovie = {"name": "Test"}
|
||||
mixed: MixedMovie = {"name": "Test"}
|
||||
|
||||
# Required keys cannot be deleted.
|
||||
# error: [invalid-argument-type]
|
||||
del m["name"]
|
||||
```
|
||||
|
||||
TODO: Deletion of `NotRequired` keys (or keys in `total=False` TypedDicts) should be allowed, but ty
|
||||
currently uses the typeshed stub `__delitem__(k: Never)` which rejects all deletions. See
|
||||
mypy/pyright behavior for reference.
|
||||
# In a partial TypedDict (`total=False`), all keys can be deleted.
|
||||
del p["name"]
|
||||
|
||||
# `NotRequired` keys can always be deleted.
|
||||
del mixed["year"]
|
||||
|
||||
# But required keys in mixed `TypedDict` still cannot be deleted.
|
||||
# error: [invalid-argument-type]
|
||||
del mixed["name"]
|
||||
|
||||
# And keys that don't exist cannot be deleted.
|
||||
# error: [invalid-argument-type]
|
||||
del mixed["non_existent"]
|
||||
```
|
||||
|
||||
@@ -2818,6 +2818,62 @@ impl<'db> ClassLiteral<'db> {
|
||||
CallableTypeKind::FunctionLike,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__delitem__") => {
|
||||
let fields = self.fields(db, specialization, field_policy);
|
||||
|
||||
// Only non-required fields can be deleted. Required fields cannot be deleted
|
||||
// because that would violate the TypedDict's structural type.
|
||||
let mut deletable_fields = fields
|
||||
.iter()
|
||||
.filter(|(_, field)| !field.is_required())
|
||||
.peekable();
|
||||
|
||||
if deletable_fields.peek().is_none() {
|
||||
// If there are no deletable fields (all fields are required), synthesize a
|
||||
// `__delitem__` that takes a `key` of type `Never` to signal that no keys
|
||||
// can be deleted.
|
||||
return Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::single(Signature::new(
|
||||
Parameters::new(
|
||||
db,
|
||||
[
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(Type::Never),
|
||||
],
|
||||
),
|
||||
Some(Type::none(db)),
|
||||
)),
|
||||
CallableTypeKind::FunctionLike,
|
||||
)));
|
||||
}
|
||||
|
||||
// Otherwise, add overloads for all deletable fields.
|
||||
let overloads = deletable_fields.map(|(name, _field)| {
|
||||
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
Signature::new(
|
||||
Parameters::new(
|
||||
db,
|
||||
[
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
],
|
||||
),
|
||||
Some(Type::none(db)),
|
||||
)
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
CallableTypeKind::FunctionLike,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "get") => {
|
||||
let overloads = self
|
||||
.fields(db, specialization, field_policy)
|
||||
|
||||
Reference in New Issue
Block a user