Repository: quora/pyanalyze
Pull request #517 (2022-04-12 00:15:19)

Remove WeakExtension
With the new support for variable-length elements in SequenceValue,
we can use a SequenceValue with a single variable-length element
instead of a "weak" list. The behavior should be roughly equivalent.



Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 91dfa7b..4ae3fe9 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Add implementation function for `dict.pop` (#517)
+- Remove `WeakExtension` (#517)
 - Fix propagation of no-return-unless constraints from calls
   to unions (#518)
 - Initial support for variable-length heterogeneous sequences
diff --git a/pyanalyze/arg_spec.py b/pyanalyze/arg_spec.py
index 80e02b4..ad772eb 100644
--- a/pyanalyze/arg_spec.py
+++ b/pyanalyze/arg_spec.py
@@ -50,7 +50,6 @@ from .value import (
     Value,
     TypeVarValue,
     extract_typevars,
-    make_weak,
 )
 import pyanalyze
 
@@ -206,7 +205,7 @@ class FunctionsSafeToCall(PyObjectSequenceOption[object]):
     arguments."""
 
     name = "functions_safe_to_call"
-    default_value = [sorted, asynq.asynq, make_weak]
+    default_value = [sorted, asynq.asynq]
 
 
 _HookReturn = Union[None, ConcreteSignature, inspect.Signature, Callable[..., Any]]
diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py
index bd1f9cf..7d739d8 100644
--- a/pyanalyze/implementation.py
+++ b/pyanalyze/implementation.py
@@ -47,11 +47,9 @@ from .value import (
     MultiValuedValue,
     KNOWN_MUTABLE_TYPES,
     Value,
-    WeakExtension,
     check_hashability,
     concrete_values_from_iterable,
     kv_pairs_from_mapping,
-    make_weak,
     unannotate,
     unite_values,
     flatten_values,
@@ -365,15 +363,8 @@ def _list_append_impl(ctx: CallContext) -> ImplReturn:
             )
             return ImplReturn(KnownValue(None), no_return_unless=no_return_unless)
         elif isinstance(lst, GenericValue):
-            return _maybe_broaden_weak_type(
-                "list.append",
-                "object",
-                ctx.vars["self"],
-                lst,
-                element,
-                ctx,
-                list,
-                varname,
+            return _check_generic_container(
+                "list.append", "object", ctx.vars["self"], lst, element, ctx, list
             )
     return ImplReturn(KnownValue(None))
 
@@ -659,6 +650,90 @@ def _dict_get_impl(ctx: CallContext) -> ImplReturn:
     return flatten_unions(inner, ctx.vars["key"])
 
 
+def _dict_pop_impl(ctx: CallContext) -> ImplReturn:
+    key = ctx.vars["key"]
+    default = ctx.vars["default"]
+    varname = ctx.visitor.varname_for_self_constraint(ctx.node)
+    self_value = replace_known_sequence_value(ctx.vars["self"])
+
+    if not _check_dict_key_hashability(key, ctx, "key"):
+        return ImplReturn(AnyValue(AnySource.error))
+
+    if isinstance(self_value, TypedDictValue):
+        if not TypedValue(str).is_assignable(key, ctx.visitor):
+            ctx.show_error(
+                f"TypedDict key must be str, not {key}",
+                ErrorCode.invalid_typeddict_key,
+                arg="key",
+            )
+            return ImplReturn(AnyValue(AnySource.error))
+        elif isinstance(key, KnownValue):
+            try:
+                is_required, expected_type = self_value.items[key.val]
+            # probably KeyError, but catch anything in case it's an
+            # unhashable str subclass or something
+            except Exception:
+                pass
+            else:
+                if is_required:
+                    ctx.show_error(
+                        f"Cannot pop required TypedDict key {key}",
+                        error_code=ErrorCode.incompatible_argument,
+                        arg="key",
+                    )
+                return ImplReturn(_maybe_unite(expected_type, default))
+        ctx.show_error(
+            f"Key {key} does not exist in TypedDict",
+            ErrorCode.invalid_typeddict_key,
+            arg="key",
+        )
+        return ImplReturn(default)
+    elif isinstance(self_value, DictIncompleteValue):
+        existing_value = self_value.get_value(key, ctx.visitor)
+        is_present = existing_value is not UNINITIALIZED_VALUE
+        if varname is not None and isinstance(key, KnownValue):
+            new_value = DictIncompleteValue(
+                self_value.typ,
+                [pair for pair in self_value.kv_pairs if pair.key != key],
+            )
+            no_return_unless = Constraint(
+                varname, ConstraintType.is_value_object, True, new_value
+            )
+        else:
+            no_return_unless = NULL_CONSTRAINT
+        if not is_present:
+            if default is _NO_ARG_SENTINEL:
+                ctx.show_error(
+                    f"Key {key} does not exist in dictionary {self_value}",
+                    error_code=ErrorCode.incompatible_argument,
+                    arg="key",
+                )
+                return ImplReturn(AnyValue(AnySource.error))
+            return ImplReturn(default, no_return_unless=no_return_unless)
+        return ImplReturn(
+            _maybe_unite(existing_value, default), no_return_unless=no_return_unless
+        )
+    elif isinstance(self_value, TypedValue):
+        key_type = self_value.get_generic_arg_for_type(dict, ctx.visitor, 0)
+        value_type = self_value.get_generic_arg_for_type(dict, ctx.visitor, 1)
+        tv_map = key_type.can_assign(key, ctx.visitor)
+        if isinstance(tv_map, CanAssignError):
+            ctx.show_error(
+                f"Key {key} is not valid for {self_value}",
+                ErrorCode.incompatible_argument,
+                arg="key",
+            )
+        return ImplReturn(_maybe_unite(value_type, default))
+    else:
+        return ImplReturn(AnyValue(AnySource.inference))
+
+
+def _maybe_unite(value: Value, default: Value) -> Value:
+    if default is _NO_ARG_SENTINEL:
+        return value
+    return unite_values(value, default)
+
+
 def _dict_setdefault_impl(ctx: CallContext) -> ImplReturn:
     key = ctx.vars["key"]
     default = ctx.vars["default"]
@@ -721,27 +796,14 @@ def _dict_setdefault_impl(ctx: CallContext) -> ImplReturn:
         key_type = self_value.get_generic_arg_for_type(dict, ctx.visitor, 0)
         value_type = self_value.get_generic_arg_for_type(dict, ctx.visitor, 1)
         new_value_type = unite_values(value_type, default)
-        if _is_weak(ctx.vars["self"]):
-            new_key_type = unite_values(key_type, key)
-            new_type = make_weak(
-                GenericValue(self_value.typ, [new_key_type, new_value_type])
+        tv_map = key_type.can_assign(key, ctx.visitor)
+        if isinstance(tv_map, CanAssignError):
+            ctx.show_error(
+                f"Key {key} is not valid for {self_value}",
+                ErrorCode.incompatible_argument,
+                arg="key",
             )
-            if varname is not None:
-                no_return_unless = Constraint(
-                    varname, ConstraintType.is_value_object, True, new_type
-                )
-            else:
-                no_return_unless = NULL_CONSTRAINT
-            return ImplReturn(new_value_type, no_return_unless=no_return_unless)
-        else:
-            tv_map = key_type.can_assign(key, ctx.visitor)
-            if isinstance(tv_map, CanAssignError):
-                ctx.show_error(
-                    f"Key {key} is not valid for {self_value}",
-                    ErrorCode.incompatible_argument,
-                    arg="key",
-                )
-            return ImplReturn(new_value_type)
+        return ImplReturn(new_value_type)
     else:
         return ImplReturn(AnyValue(AnySource.inference))
 
@@ -767,7 +829,7 @@ def _unpack_iterable_of_pairs(
     return kv_pairs
 
 
-def _weak_dict_update(
+def _update_incomplete_dict(
     self_val: Value,
     pairs: Sequence[KVPair],
     ctx: CallContext,
@@ -799,17 +861,13 @@ def _add_pairs_to_dict(
     ctx: CallContext,
     varname: Optional[VarnameWithOrigin],
 ) -> ImplReturn:
-    if _is_weak(self_val):
-        return _weak_dict_update(self_val, pairs, ctx, varname)
-
-    # Now we don't care about Annotated
     self_val = replace_known_sequence_value(self_val)
     if isinstance(self_val, TypedDictValue):
         for pair in pairs:
             _typeddict_setitem(self_val, pair.key, pair.value, ctx)
         return ImplReturn(KnownValue(None))
     elif isinstance(self_val, DictIncompleteValue):
-        return _weak_dict_update(self_val, pairs, ctx, varname)
+        return _update_incomplete_dict(self_val, pairs, ctx, varname)
     elif isinstance(self_val, TypedValue):
         key_type = self_val.get_generic_arg_for_type(dict, ctx.visitor, 0)
         value_type = self_val.get_generic_arg_for_type(dict, ctx.visitor, 1)
@@ -832,7 +890,7 @@ def _add_pairs_to_dict(
                 )
         return ImplReturn(KnownValue(None))
     else:
-        return _weak_dict_update(self_val, pairs, ctx, varname)
+        return _update_incomplete_dict(self_val, pairs, ctx, varname)
 
 
 def _dict_update_impl(ctx: CallContext) -> ImplReturn:
@@ -949,7 +1007,7 @@ def _list_extend_or_iadd_impl(
             actual_type = iterable.get_generic_arg_for_type(
                 collections.abc.Iterable, ctx.visitor, 0
             )
-            return _maybe_broaden_weak_type(
+            return _check_generic_container(
                 name,
                 iterable_arg,
                 lst,
@@ -957,7 +1015,6 @@ def _list_extend_or_iadd_impl(
                 actual_type,
                 ctx,
                 list,
-                varname,
                 return_container=return_container,
             )
         return ImplReturn(lst if return_container else KnownValue(None))
@@ -973,33 +1030,18 @@ def _list_iadd_impl(ctx: CallContext) -> ImplReturn:
     return _list_extend_or_iadd_impl(ctx, "x", "list.__iadd__", return_container=True)
 
 
-def _is_weak(val: Value) -> bool:
-    return isinstance(val, AnnotatedValue) and val.has_metadata_of_type(WeakExtension)
-
-
-def _maybe_broaden_weak_type(
+def _check_generic_container(
     function_name: str,
     arg: str,
     original_container_type: Value,
-    container_type: Value,
+    container_type: GenericValue,
     actual_type: Value,
     ctx: CallContext,
     typ: type,
-    varname: VarnameWithOrigin,
     *,
     return_container: bool = False,
 ) -> ImplReturn:
     expected_type = container_type.get_generic_arg_for_type(typ, ctx.visitor, 0)
-    if _is_weak(original_container_type):
-        generic_arg = unite_values(expected_type, actual_type)
-        constrained_value = make_weak(GenericValue(typ, [generic_arg]))
-        no_return_unless = Constraint(
-            varname, ConstraintType.is_value_object, True, constrained_value
-        )
-        if return_container:
-            return ImplReturn(constrained_value)
-        return ImplReturn(KnownValue(None), no_return_unless=no_return_unless)
-
     tv_map = expected_type.can_assign(actual_type, ctx.visitor)
     if isinstance(tv_map, CanAssignError):
         ctx.show_error(
@@ -1029,15 +1071,8 @@ def _set_add_impl(ctx: CallContext) -> ImplReturn:
             )
             return ImplReturn(KnownValue(None), no_return_unless=no_return_unless)
         elif isinstance(set_value, GenericValue):
-            return _maybe_broaden_weak_type(
-                "set.add",
-                "object",
-                ctx.vars["self"],
-                set_value,
-                element,
-                ctx,
-                set,
-                varname,
+            return _check_generic_container(
+                "set.add", "object", ctx.vars["self"], set_value, element, ctx, set
             )
     return ImplReturn(KnownValue(None))
 
@@ -1533,6 +1568,15 @@ def get_default_argspecs() -> Dict[object, Signature]:
             callable=dict.setdefault,
             impl=_dict_setdefault_impl,
         ),
+        Signature.make(
+            [
+                SigParameter("self", _POS_ONLY, annotation=TypedValue(dict)),
+                SigParameter("key", _POS_ONLY),
+                SigParameter("default", _POS_ONLY, default=_NO_ARG_SENTINEL),
+            ],
+            callable=dict.pop,
+            impl=_dict_pop_impl,
+        ),
         Signature.make(
             [
                 SigParameter("self", _POS_ONLY, annotation=TypedValue(dict)),
@@ -1551,9 +1595,8 @@ def get_default_argspecs() -> Dict[object, Signature]:
                     annotation=GenericValue(dict, [TypeVarValue(K), TypeVarValue(V)]),
                 )
             ],
-            AnnotatedValue(
-                GenericValue(dict, [TypeVarValue(K), TypeVarValue(V)]),
-                [WeakExtension()],
+            DictIncompleteValue(
+                dict, [KVPair(TypeVarValue(K), TypeVarValue(V), is_many=True)]
             ),
             callable=dict.copy,
         ),
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index f61ac94..d57d101 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -167,7 +167,6 @@ from .value import (
     get_tv_map,
     is_union,
     kv_pairs_from_mapping,
-    make_weak,
     set_self,
     unannotate_value,
     unite_and_simplify,
@@ -2468,10 +2467,9 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
                         detail=str(hashability),
                     )
                     key_value = AnyValue(AnySource.error)
-            if isinstance(key_value, AnyValue) and isinstance(value_value, AnyValue):
-                return TypedValue(dict)
-            else:
-                return make_weak(GenericValue(dict, [key_value, value_value]))
+            return DictIncompleteValue(
+                dict, [KVPair(key_value, value_value, is_many=True)]
+            )
 
         with qcore.override(self, "in_comprehension_body", True):
             member_value = self.visit(node.elt)
@@ -2489,7 +2487,9 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
 
         if typ is types.GeneratorType:
             return GenericValue(typ, [member_value, KnownValue(None), KnownValue(None)])
-        return make_weak(GenericValue(typ, [member_value]))
+        # Returning a SequenceValue here instead of a GenericValue allows
+        # later code to modify this container.
+        return SequenceValue(typ, [(True, member_value)])
 
     # Literals and displays
 
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index 775589a..0750562 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -2001,22 +2001,6 @@ class NoReturnConstraintExtension(Extension):
         return id(self)
 
 
-@dataclass(frozen=True)
-class WeakExtension(Extension):
-    """Used to indicate that a generic argument to a container may be widened.
-
-    This is used only in conjuction with the special casing for functions
-    like ``list.extend``. After code like ``lst = [1, 2]; lst.extend([i for in range(5)])``
-    we may end up inferring a type like ``List[Literal[0, 1, 2, 3, 4]]``, but that is
-    too narrow and leads to false positives if later code puts a different int in
-    the list. If the generic argument is instead annotated with ``WeakExtension``, we
-    widen the type to accommodate later appends.
-
-    The ``TestGenericMutators.test_weak_value`` test case is an example.
-
-    """
-
-
 @dataclass(frozen=True)
 class AlwaysPresentExtension(Extension):
     """Extension that indicates that an iterable value is nonempty.
@@ -2237,10 +2221,6 @@ def intersect_bounds_maps(bounds_maps: Sequence[BoundsMap]) -> BoundsMap:
     }
 
 
-def make_weak(val: Value) -> Value:
-    return annotate_value(val, [WeakExtension()])
-
-
 def annotate_value(origin: Value, metadata: Sequence[Union[Value, Extension]]) -> Value:
     if not metadata:
         return origin


Test patch:
diff --git a/pyanalyze/test_async_await.py b/pyanalyze/test_async_await.py
index 1595910..1c477ea 100644
--- a/pyanalyze/test_async_await.py
+++ b/pyanalyze/test_async_await.py
@@ -1,6 +1,13 @@
 # static analysis: ignore
 from .tests import make_simple_sequence
-from .value import GenericValue, KnownValue, TypedValue, make_weak, AnyValue, AnySource
+from .value import (
+    GenericValue,
+    KnownValue,
+    SequenceValue,
+    TypedValue,
+    AnyValue,
+    AnySource,
+)
 from .implementation import assert_is_value
 from .test_node_visitor import assert_passes, only_before
 from .test_name_check_visitor import TestNameCheckVisitorBase
@@ -62,7 +69,7 @@ class TestAsyncAwait(TestNameCheckVisitorBase):
 
         async def f():
             x = [y async for y in AIter()]
-            assert_is_value(x, make_weak(GenericValue(list, [TypedValue(int)])))
+            assert_is_value(x, SequenceValue(list, [(True, TypedValue(int))]))
 
     @assert_passes()
     def test_async_generator(self):
@@ -82,7 +89,7 @@ class TestAsyncAwait(TestNameCheckVisitorBase):
             # TODO should be list[int] but we lose the type argument somewhere
             assert_is_value(
                 ints,
-                make_weak(GenericValue(list, [AnyValue(AnySource.generic_argument)])),
+                SequenceValue(list, [(True, AnyValue(AnySource.generic_argument))]),
             )
 
     @assert_passes()
diff --git a/pyanalyze/test_asynq.py b/pyanalyze/test_asynq.py
index 6dd8367..9d5953a 100644
--- a/pyanalyze/test_asynq.py
+++ b/pyanalyze/test_asynq.py
@@ -6,9 +6,7 @@ from .value import (
     AnyValue,
     AsyncTaskIncompleteValue,
     KnownValue,
-    MultiValuedValue,
     TypedValue,
-    GenericValue,
     DictIncompleteValue,
     KVPair,
 )
@@ -93,11 +91,14 @@ class TestUnwrapYield(TestNameCheckVisitorBase):
             vals4 = yield {i: square.asynq(i) for i in ints}
             assert_is_value(
                 vals4,
-                GenericValue(
+                DictIncompleteValue(
                     dict,
                     [
-                        MultiValuedValue([KnownValue(0), KnownValue(1), KnownValue(2)]),
-                        TypedValue(int),
+                        KVPair(
+                            KnownValue(0) | KnownValue(1) | KnownValue(2),
+                            TypedValue(int),
+                            is_many=True,
+                        )
                     ],
                 ),
             )
diff --git a/pyanalyze/test_implementation.py b/pyanalyze/test_implementation.py
index 68af919..9c11000 100644
--- a/pyanalyze/test_implementation.py
+++ b/pyanalyze/test_implementation.py
@@ -4,7 +4,6 @@ from .test_node_visitor import assert_passes
 from .tests import make_simple_sequence
 from .value import (
     NO_RETURN_VALUE,
-    AnnotatedValue,
     AnySource,
     AnyValue,
     KVPair,
@@ -17,7 +16,6 @@ from .value import (
     DictIncompleteValue,
     SubclassValue,
     MultiValuedValue,
-    make_weak,
 )
 
 
@@ -635,36 +633,28 @@ class TestGenericMutators(TestNameCheckVisitorBase):
                 lst = [2 for _ in cond]
             assert_is_value(
                 lst,
-                MultiValuedValue(
-                    [
-                        make_weak(GenericValue(list, [KnownValue(1)])),
-                        make_weak(GenericValue(list, [KnownValue(2)])),
-                    ]
-                ),
+                SequenceValue(list, [(True, KnownValue(1))])
+                | SequenceValue(list, [(True, KnownValue(2))]),
             )
             lst.extend([3, 4])
 
             assert_is_value(
                 lst,
-                make_weak(
-                    GenericValue(
-                        list,
-                        [
-                            MultiValuedValue(
-                                [KnownValue(2), KnownValue(3), KnownValue(4)]
-                            )
-                        ],
-                    )
+                SequenceValue(
+                    list,
+                    [
+                        (True, KnownValue(1)),
+                        (False, KnownValue(3)),
+                        (False, KnownValue(4)),
+                    ],
                 )
-                | make_weak(
-                    GenericValue(
-                        list,
-                        [
-                            MultiValuedValue(
-                                [KnownValue(1), KnownValue(3), KnownValue(4)]
-                            )
-                        ],
-                    )
+                | SequenceValue(
+                    list,
+                    [
+                        (True, KnownValue(2)),
+                        (False, KnownValue(3)),
+                        (False, KnownValue(4)),
+                    ],
                 ),
             )
 
@@ -754,28 +744,32 @@ class TestGenericMutators(TestNameCheckVisitorBase):
             weak_dict = {i: str(i) for i in ints}
             assert_is_value(
                 weak_dict,
-                make_weak(GenericValue(dict, [TypedValue(int), TypedValue(str)])),
+                DictIncompleteValue(
+                    dict, [KVPair(TypedValue(int), TypedValue(str), is_many=True)]
+                ),
             )
             assert_is_value(weak_dict.setdefault(3, str(TD)), TypedValue(str))
 
-            int_or_3 = MultiValuedValue([TypedValue(int), KnownValue(3)])
             assert_is_value(
-                weak_dict, make_weak(GenericValue(dict, [int_or_3, TypedValue(str)]))
-            )
-            assert_is_value(
-                weak_dict.setdefault(3),
-                MultiValuedValue([TypedValue(str), KnownValue(None)]),
+                weak_dict,
+                DictIncompleteValue(
+                    dict,
+                    [
+                        KVPair(TypedValue(int), TypedValue(str), is_many=True),
+                        KVPair(KnownValue(3), TypedValue(str), is_required=False),
+                    ],
+                ),
             )
+            assert_is_value(weak_dict.setdefault(3), TypedValue(str) | KnownValue(None))
             assert_is_value(
                 weak_dict,
-                make_weak(
-                    GenericValue(
-                        dict,
-                        [
-                            int_or_3,
-                            MultiValuedValue([TypedValue(str), KnownValue(None)]),
-                        ],
-                    )
+                DictIncompleteValue(
+                    dict,
+                    [
+                        KVPair(TypedValue(int), TypedValue(str), is_many=True),
+                        KVPair(KnownValue(3), TypedValue(str), is_required=False),
+                        KVPair(KnownValue(3), KnownValue(None), is_required=False),
+                    ],
                 ),
             )
 
@@ -790,6 +784,65 @@ class TestGenericMutators(TestNameCheckVisitorBase):
             )
             assert_is_value(strong_dict, expected)
 
+    @assert_passes()
+    def test_dict_pop(self):
+        from typing_extensions import TypedDict, NotRequired
+        from typing import Dict
+
+        class TD(TypedDict):
+            a: int
+            b: NotRequired[str]
+
+        def typeddict(td: TD):
+            td.pop({})  # E: unhashable_key
+            td.pop(0)  # E: invalid_typeddict_key
+            td.pop("c")  # E: invalid_typeddict_key
+            val = td.pop("a", "s")  # E: incompatible_argument
+            assert_is_value(val, TypedValue(int) | KnownValue("s"))
+            assert_is_value(td.pop("b", "x"), TypedValue(str) | KnownValue("x"))
+
+        def dict_incomplete_value():
+            incomplete_value = {"a": str(TD), "c": 42}
+            assert_is_value(
+                incomplete_value,
+                DictIncompleteValue(
+                    dict,
+                    [
+                        KVPair(KnownValue("a"), TypedValue(str)),
+                        KVPair(KnownValue("c"), KnownValue(42)),
+                    ],
+                ),
+            )
+            incomplete_value.pop("b")  # E: incompatible_argument
+            assert_is_value(incomplete_value.pop("a"), TypedValue(str))
+            assert_is_value(
+                incomplete_value,
+                DictIncompleteValue(dict, [KVPair(KnownValue("c"), KnownValue(42))]),
+            )
+            assert_is_value(
+                incomplete_value.pop("c", 1), KnownValue(42) | KnownValue(1)
+            )
+            assert_is_value(incomplete_value, DictIncompleteValue(dict, []))
+
+        def strong_typed(strong_dict: Dict[int, str]):
+            expected = GenericValue(dict, [TypedValue(int), TypedValue(str)])
+            assert_is_value(strong_dict, expected)
+            assert_is_value(strong_dict.pop(3, 42), TypedValue(str) | KnownValue(42))
+            assert_is_value(strong_dict, expected)
+            assert_is_value(strong_dict.pop(3), TypedValue(str))
+            assert_is_value(strong_dict, expected)
+
+    @assert_passes()
+    def test_dict_pop_union(self):
+        def capybara(cond):
+            d = {"a": 1, "b": 2}
+            if cond:
+                d["c"] = 3
+            else:
+                d["d"] = 4
+
+            assert_is_value(d.pop("a"), KnownValue(1))
+
     @assert_passes()
     def test_dict_update(self):
         def capybara():
@@ -824,7 +877,6 @@ class TestGenericMutators(TestNameCheckVisitorBase):
     @assert_passes()
     def test_copy_and_update(self):
         from typing import Dict
-        from pyanalyze.value import WeakExtension
 
         def capybara():
             d1: Dict[str, int] = {"x": 1}
@@ -832,7 +884,12 @@ class TestGenericMutators(TestNameCheckVisitorBase):
             assert_is_value(d1, d1_val)
             d1[1] = 3  # E: incompatible_argument
             d2 = d1.copy()
-            assert_is_value(d2, AnnotatedValue(d1_val, [WeakExtension()]))
+            assert_is_value(
+                d2,
+                DictIncompleteValue(
+                    dict, [KVPair(TypedValue(str), TypedValue(int), is_many=True)]
+                ),
+            )
             d2[1] = 3
             assert_is_value(
                 d2,
@@ -1181,15 +1238,12 @@ class TestDictSetItem(TestNameCheckVisitorBase):
 
     @assert_passes()
     def test_weak(self):
-        from pyanalyze.value import WeakExtension
-
         def capybara(arg):
             dct = {int(k): 1 for k in arg}
             assert_is_value(
                 dct,
-                AnnotatedValue(
-                    GenericValue(dict, [TypedValue(int), KnownValue(1)]),
-                    [WeakExtension()],
+                DictIncompleteValue(
+                    dict, [KVPair(TypedValue(int), KnownValue(1), is_many=True)]
                 ),
             )
             dct["x"] = "y"
diff --git a/pyanalyze/test_name_check_visitor.py b/pyanalyze/test_name_check_visitor.py
index 096f9ef..069b224 100644
--- a/pyanalyze/test_name_check_visitor.py
+++ b/pyanalyze/test_name_check_visitor.py
@@ -38,7 +38,6 @@ from .value import (
     SubclassValue,
     GenericValue,
     TypedDictValue,
-    make_weak,
 )
 from .tests import (
     proxied_fn,
@@ -149,7 +148,6 @@ def _make_module(code_str: str) -> types.ModuleType:
         TypedDictValue=TypedDictValue,
         TypeVarValue=TypeVarValue,
         dump_value=dump_value,
-        make_weak=make_weak,
         make_simple_sequence=make_simple_sequence,
         UNINITIALIZED_VALUE=UNINITIALIZED_VALUE,
         NO_RETURN_VALUE=NO_RETURN_VALUE,
@@ -970,7 +968,7 @@ class TestComprehensions(TestNameCheckVisitorBase):
 
         def capybara(lst):
             a = [int(x) for x in lst]
-            assert_is_value(a, make_weak(GenericValue(list, [TypedValue(int)])))
+            assert_is_value(a, SequenceValue(list, [(True, TypedValue(int))]))
 
             b = (0 for _ in lst)
             assert_is_value(
@@ -983,7 +981,10 @@ class TestComprehensions(TestNameCheckVisitorBase):
 
             c = {int(x): int(x) for x in lst}
             assert_is_value(
-                c, make_weak(GenericValue(dict, [TypedValue(int), TypedValue(int)]))
+                c,
+                DictIncompleteValue(
+                    dict, [KVPair(TypedValue(int), TypedValue(int), is_many=True)]
+                ),
             )
 
     @assert_passes()
@@ -1010,15 +1011,14 @@ class TestComprehensions(TestNameCheckVisitorBase):
     def test_hashability(self):
         def capybara(it):
             x = {set() for _ in it}  # E: unhashable_key
-            assert_is_value(
-                x, make_weak(GenericValue(set, [AnyValue(AnySource.error)]))
-            )
+            assert_is_value(x, SequenceValue(set, [(True, AnyValue(AnySource.error))]))
 
             y = {set(): 3 for _ in it}  # E: unhashable_key
             assert_is_value(
                 y,
-                make_weak(
-                    GenericValue(dict, [AnyValue(AnySource.error), KnownValue(3)])
+                DictIncompleteValue(
+                    dict,
+                    [KVPair(AnyValue(AnySource.error), KnownValue(3), is_many=True)],
                 ),
             )
 
@@ -1113,8 +1113,8 @@ class TestIterationTarget(TestNameCheckVisitorBase):
 
         def capybara(ints: Sequence[Literal[1, 2]]):
             lst = [x for x in ints]
-            mvv = MultiValuedValue([KnownValue(1), KnownValue(2)])
-            assert_is_value(lst, make_weak(GenericValue(list, [mvv])))
+            mvv = KnownValue(1) | KnownValue(2)
+            assert_is_value(lst, SequenceValue(list, [(True, mvv)]))
             for y in lst:
                 assert_is_value(y, mvv)
 
@@ -1126,7 +1126,7 @@ class TestIterationTarget(TestNameCheckVisitorBase):
             lst3 = [i + j * 10 for i in range(2) for j in range(3)]
             # TODO: should be list[int] instead
             assert_is_value(
-                lst3, make_weak(GenericValue(list, [AnyValue(AnySource.inference)]))
+                lst3, SequenceValue(list, [(True, AnyValue(AnySource.inference))])
             )
 
     @assert_passes()
@@ -1136,8 +1136,10 @@ class TestIterationTarget(TestNameCheckVisitorBase):
 
         def capybara(ints: Sequence[Literal[1, 2, 3]]):
             dct = {x: x for x in ints}
-            mvv = MultiValuedValue([KnownValue(1), KnownValue(2), KnownValue(3)])
-            assert_is_value(dct, make_weak(GenericValue(dict, [mvv, mvv])))
+            mvv = KnownValue(1) | KnownValue(2) | KnownValue(3)
+            assert_is_value(
+                dct, DictIncompleteValue(dict, [KVPair(mvv, mvv, is_many=True)])
+            )
 
             for key in dct:
                 assert_is_value(key, mvv)
diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py
index 181f2c0..a4cfdf2 100644
--- a/pyanalyze/test_signature.py
+++ b/pyanalyze/test_signature.py
@@ -9,9 +9,9 @@ from .value import (
     CanAssignError,
     GenericValue,
     KnownValue,
+    SequenceValue,
     TypedDictValue,
     TypedValue,
-    make_weak,
 )
 from .implementation import assert_is_value
 from .test_name_check_visitor import TestNameCheckVisitorBase
@@ -507,8 +507,7 @@ class TestCalls(TestNameCheckVisitorBase):
         def run(elts):
             lst = [x for x in elts]
             assert_is_value(
-                lst,
-                make_weak(GenericValue(list, [AnyValue(AnySource.generic_argument)])),
+                lst, SequenceValue(list, [(True, AnyValue(AnySource.generic_argument))])
             )
             lst()  # E: not_callable
 
diff --git a/pyanalyze/test_stacked_scopes.py b/pyanalyze/test_stacked_scopes.py
index 7f79df7..7cd965e 100644
--- a/pyanalyze/test_stacked_scopes.py
+++ b/pyanalyze/test_stacked_scopes.py
@@ -14,10 +14,10 @@ from .value import (
     KnownValue,
     MultiValuedValue,
     ReferencingValue,
+    SequenceValue,
     TypedValue,
     UNINITIALIZED_VALUE,
     assert_is_value,
-    make_weak,
 )
 
 
@@ -992,8 +992,6 @@ class TestConstraints(TestNameCheckVisitorBase):
 
     @assert_passes()
     def test_nested_scope(self):
-        from pyanalyze.value import WeakExtension
-
         class A:
             pass
 
@@ -1004,12 +1002,7 @@ class TestConstraints(TestNameCheckVisitorBase):
             if isinstance(a, B):
                 assert_is_value(a, TypedValue(B))
                 lst = [a for _ in iterable]
-                assert_is_value(
-                    lst,
-                    AnnotatedValue(
-                        GenericValue(list, [TypedValue(B)]), [WeakExtension()]
-                    ),
-                )
+                assert_is_value(lst, SequenceValue(list, [(True, TypedValue(B))]))
 
     @assert_passes()
     def test_qcore_asserts(self):
@@ -1419,21 +1412,14 @@ class TestConstraints(TestNameCheckVisitorBase):
                 return None
 
         def capybara(x, y):
-            assert_is_value(
-                maybe_int(x), MultiValuedValue([TypedValue(int), KnownValue(None)])
-            )
+            assert_is_value(maybe_int(x), TypedValue(int) | KnownValue(None))
 
             lst = [maybe_int(elt) for elt in y]
             assert_is_value(
-                lst,
-                make_weak(
-                    GenericValue(
-                        list, [MultiValuedValue([TypedValue(int), KnownValue(None)])]
-                    )
-                ),
+                lst, SequenceValue(list, [(True, TypedValue(int) | KnownValue(None))])
             )
             lst2 = [elt for elt in lst if elt]
-            assert_is_value(lst2, make_weak(GenericValue(list, [TypedValue(int)])))
+            assert_is_value(lst2, SequenceValue(list, [(True, TypedValue(int))]))
 
     @assert_passes()
     def test_comprehension_composite(self):
@@ -1447,37 +1433,29 @@ class TestConstraints(TestNameCheckVisitorBase):
         def use_attr(c: List[Capybara]) -> None:
             assert_is_value(
                 [elt.x for elt in c],
-                make_weak(
-                    GenericValue(
-                        list, [MultiValuedValue([TypedValue(int), KnownValue(None)])]
-                    )
-                ),
+                SequenceValue(list, [(True, TypedValue(int) | KnownValue(None))]),
             )
             assert_is_value(
                 [elt.x for elt in c if elt.x is not None],
-                make_weak(GenericValue(list, [TypedValue(int)])),
+                SequenceValue(list, [(True, TypedValue(int))]),
             )
             assert_is_value(
                 [elt.x for elt in c if elt.x],
-                make_weak(GenericValue(list, [TypedValue(int)])),
+                SequenceValue(list, [(True, TypedValue(int))]),
             )
 
         def use_subscript(d: List[Tuple[int, Optional[int]]]) -> None:
             assert_is_value(
                 [pair[1] for pair in d],
-                make_weak(
-                    GenericValue(
-                        list, [MultiValuedValue([TypedValue(int), KnownValue(None)])]
-                    )
-                ),
+                SequenceValue(list, [(True, TypedValue(int) | KnownValue(None))]),
             )
             assert_is_value(
                 [pair[1] for pair in d if pair[1] is not None],
-                make_weak(GenericValue(list, [TypedValue(int)])),
+                SequenceValue(list, [(True, TypedValue(int))]),
             )
             assert_is_value(
                 [pair[1] for pair in d if pair[1]],
-                make_weak(GenericValue(list, [TypedValue(int)])),
+                SequenceValue(list, [(True, TypedValue(int))]),
             )
 
     @assert_passes()
diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py
index 67a801a..b047e6e 100644
--- a/pyanalyze/test_typeshed.py
+++ b/pyanalyze/test_typeshed.py
@@ -27,6 +27,7 @@ from .tests import make_simple_sequence
 from .typeshed import TypeshedFinder
 from .value import (
     CallableValue,
+    DictIncompleteValue,
     SubclassValue,
     TypedDictValue,
     assert_is_value,
@@ -36,7 +37,7 @@ from .value import (
     NewTypeValue,
     TypedValue,
     GenericValue,
-    make_weak,
+    KVPair,
     TypeVarValue,
     UNINITIALIZED_VALUE,
     Value,
@@ -130,7 +131,9 @@ class TestTypeshedClient(TestNameCheckVisitorBase):
         def capybara(x: Dict[int, str]):
             assert_is_value(
                 {k: v for k, v in x.items()},
-                make_weak(GenericValue(dict, [TypedValue(int), TypedValue(str)])),
+                DictIncompleteValue(
+                    dict, [KVPair(TypedValue(int), TypedValue(str), is_many=True)]
+                ),
             )
 
     @assert_passes()