mirror of
https://github.com/astral-sh/ruff
synced 2026-01-11 08:34:29 -05:00
## Summary We synthesize a (potentially large) set of `__setitem__` overloads for every item in a `TypedDict`. Previously, validation of subscript assignments on `TypedDict`s relied on actually calling `__setitem__` with the provided key and value types, which implied that we needed to do the full overload call evaluation for this large set of overloads. This PR improves the performance of subscript assignment checks on `TypedDict`s by validating the assignment directly instead of calling `__setitem__`. This PR also adds better handling for assignments to subscripts on union and intersection types (but does not attempt to make it perfect). It achieves this by distributing the check over unions and intersections, instead of calling `__setitem__` on the union/intersection directly. We already do something similar when validating *attribute* assignments. ## Ecosystem impact * A lot of diagnostics change their rule type, and/or split into multiple diagnostics. The new version is more verbose, but easier to understand, in my opinion * Almost all of the invalid-key diagnostics come from pydantic, and they should all go away (including many more) when we implement https://github.com/astral-sh/ty/issues/1479 * Everything else looks correct to me. There may be some new diagnostics due to the fact that we now check intersections. ## Test Plan New Markdown tests.
2.6 KiB
2.6 KiB
Instance subscript
__getitem__ unbound
class NotSubscriptable: ...
a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscriptable` with no `__getitem__` method"
__getitem__ not callable
class NotSubscriptable:
__getitem__ = None
# TODO: this would be more user-friendly if the `call-non-callable` diagnostic was
# transformed into a `not-subscriptable` diagnostic with a subdiagnostic explaining
# that this was because `__getitem__` was possibly not callable
#
# error: [call-non-callable] "Method `__getitem__` of type `Unknown | None` may not be callable on object of type `NotSubscriptable`"
a = NotSubscriptable()[0]
Valid __getitem__
class Identity:
def __getitem__(self, index: int) -> int:
return index
reveal_type(Identity()[0]) # revealed: int
__getitem__ union
def _(flag: bool):
class Identity:
if flag:
def __getitem__(self, index: int) -> int:
return index
else:
def __getitem__(self, index: int) -> str:
return str(index)
reveal_type(Identity()[0]) # revealed: int | str
__getitem__ with invalid index argument
class Identity:
def __getitem__(self, index: int) -> int:
return index
a = Identity()
# error: [invalid-argument-type] "Method `__getitem__` of type `bound method Identity.__getitem__(index: int) -> int` cannot be called with key of type `Literal["a"]` on object of type `Identity`"
a["a"]
__setitem__ with no __getitem__
class NoGetitem:
def __setitem__(self, index: int, value: int) -> None:
pass
a = NoGetitem()
a[0] = 0
Subscript store with no __setitem__
class NoSetitem: ...
a = NoSetitem()
a[0] = 0 # error: "Cannot assign to a subscript on an object of type `NoSetitem` with no `__setitem__` method"
__setitem__ not callable
class NoSetitem:
__setitem__ = None
a = NoSetitem()
a[0] = 0 # error: "Method `__setitem__` of type `Unknown | None` may not be callable on object of type `NoSetitem`"
Valid __setitem__ method
class Identity:
def __setitem__(self, index: int, value: int) -> None:
pass
a = Identity()
a[0] = 0
__setitem__ with invalid index argument
class Identity:
def __setitem__(self, index: int, value: int) -> None:
pass
a = Identity()
# error: [invalid-assignment] "Method `__setitem__` of type `bound method Identity.__setitem__(index: int, value: int) -> None` cannot be called with a key of type `Literal["a"]` and a value of type `Literal[0]` on object of type `Identity`"
a["a"] = 0