Skip to content

Commit

Permalink
Improve dict.__delitem__ impl (#726)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Feb 24, 2024
1 parent 9623e2a commit 76700c1
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add special handling for `dict.__delitem__` (#723, #726)
- Add support for the `ReadOnly` type qualifier (PEP 705) and
for the `closed=True` TypedDict argument (PEP 728) (#723)
- Fix some higher-order behavior of `TypeGuard` and `TypeIs` (#719)
Expand Down
20 changes: 8 additions & 12 deletions pyanalyze/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ def inner(key: Value) -> Value:

def _dict_delitem_impl(ctx: CallContext) -> ImplReturn:
key = ctx.vars["key"]
varname = ctx.visitor.varname_for_self_constraint(ctx.node)
varname = ctx.varname_for_arg("self")
self_value = replace_known_sequence_value(ctx.vars["self"])

if not _check_dict_key_hashability(key, ctx, "key"):
Expand All @@ -775,16 +775,16 @@ def _dict_delitem_impl(ctx: CallContext) -> ImplReturn:
except Exception:
pass
else:
if entry.required:
if entry.readonly:
ctx.show_error(
f"Cannot delete required TypedDict key {key}",
error_code=ErrorCode.incompatible_argument,
f"Cannot delete readonly TypedDict key {key}",
error_code=ErrorCode.readonly_typeddict,
arg="key",
)
elif entry.readonly:
elif entry.required:
ctx.show_error(
f"Cannot delete readonly TypedDict key {key}",
error_code=ErrorCode.readonly_typeddict,
f"Cannot delete required TypedDict key {key}",
error_code=ErrorCode.incompatible_argument,
arg="key",
)
return ImplReturn(KnownValue(None))
Expand Down Expand Up @@ -814,11 +814,7 @@ def _dict_delitem_impl(ctx: CallContext) -> ImplReturn:
else:
no_return_unless = NULL_CONSTRAINT
if not is_present:
ctx.show_error(
f"Key {key} does not exist in dictionary {self_value}",
error_code=ErrorCode.incompatible_argument,
arg="key",
)
# No error; it might have been added where we couldn't see it
return ImplReturn(KnownValue(None))
return ImplReturn(KnownValue(None), no_return_unless=no_return_unless)
elif isinstance(self_value, TypedValue):
Expand Down
90 changes: 90 additions & 0 deletions pyanalyze/test_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,96 @@ def capybara():
)


class TestDictDelitem(TestNameCheckVisitorBase):
@assert_passes()
def test_incomplete(self) -> None:
d1 = {}
d2 = {"a": 1, "b": 2}

def capybara() -> None:
del d1["a"] # ok
assert_is_value(d1, KnownValue({}))
del d2["a"] # ok
assert_is_value(
d2, DictIncompleteValue(dict, [KVPair(KnownValue("b"), KnownValue(2))])
)

def pacarana() -> None:
d = {"a": 1, "b": 2}
del d["a"]
assert_is_value(
d, DictIncompleteValue(dict, [KVPair(KnownValue("b"), KnownValue(2))])
)
del d["c"] # ok
assert_is_value(
d, DictIncompleteValue(dict, [KVPair(KnownValue("b"), KnownValue(2))])
)

@assert_passes()
def test_typed(self) -> None:
from typing import Any, Dict

def capybara(d: Dict[str, int]) -> None:
del d["x"]
del d[1] # E: incompatible_argument

def pacarana(d: Dict[Any, Any]) -> None:
del d["x"]
del d[1]
del d[{}] # E: unhashable_key

@assert_passes()
def test_typeddict(self) -> None:
from typing_extensions import NotRequired, ReadOnly, TypedDict

class TD(TypedDict):
a: str
b: NotRequired[int]
c: ReadOnly[str]

class ClosedTD(TypedDict, closed=True):
a: str
b: NotRequired[int]

class ExtraItemsTD(TypedDict, closed=True):
a: str
b: NotRequired[int]
__extra_items__: int

class ReadOnlyExtraItemsTD(TypedDict, closed=True):
a: str
b: NotRequired[int]
__extra_items__: ReadOnly[int]

def capybara(
td: TD,
closed: ClosedTD,
extra_items: ExtraItemsTD,
readonly_extra: ReadOnlyExtraItemsTD,
s: str,
) -> None:
del td[1] # E: invalid_typeddict_key
del td["a"] # E: incompatible_argument
del td["b"] # ok
del td["c"] # E: readonly_typeddict
del td[s] # E: invalid_typeddict_key

del closed["a"] # E: incompatible_argument
del closed["b"] # ok
del closed["c"] # E: invalid_typeddict_key
del closed[s] # E: invalid_typeddict_key

del extra_items["a"] # E: incompatible_argument
del extra_items["b"] # ok
del extra_items["c"] # ok
del extra_items[s] # ok

del readonly_extra["a"] # E: incompatible_argument
del readonly_extra["b"] # ok
del readonly_extra["c"] # E: readonly_typeddict
del readonly_extra[s] # E: readonly_typeddict


class TestSequenceGetItem(TestNameCheckVisitorBase):
@assert_passes()
def test_list(self):
Expand Down
2 changes: 1 addition & 1 deletion pyanalyze/test_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def capybara(td: TD, anydict: Dict[str, Any]) -> None:
td.pop("a") # E: readonly_typeddict
td.pop("b") # E: incompatible_argument
del td["a"] # E: readonly_typeddict
del td["b"] # E: incompatible_argument
del td["b"] # E: readonly_typeddict

@assert_passes()
def test_compatibility(self):
Expand Down

0 comments on commit 76700c1

Please sign in to comment.