Repository: quora/pyanalyze
Pull request #570 (2022-11-15 00:05:27)

key_type must be str after all


Patch:
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index 7253c63..7bdc08a 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -1154,19 +1154,17 @@ class TypedDictValue(GenericValue):
         self, items: Dict[str, Tuple[bool, Value]], extra_keys: Optional[Value] = None
     ) -> None:
         value_types = []
-        key_types = []
         if items:
             value_types += [val for _, val in items.values()]
-            key_types += [KnownValue(key) for key in items.keys()]
         if extra_keys is not None:
             value_types.append(extra_keys)
-            key_types.append(TypedValue(str))
         value_type = (
             unite_values(*value_types)
             if value_types
             else AnyValue(AnySource.unreachable)
         )
-        key_type = unite_values(*key_types) if key_types else TypedValue(str)
+        # The key type must be str so dict[str, Any] is compatible with a TypedDict
+        key_type = TypedValue(str)
         super().__init__(dict, (key_type, value_type))
         self.items = items
         self.extra_keys = extra_keys


Test patch:
diff --git a/pyanalyze/test_stacked_scopes.py b/pyanalyze/test_stacked_scopes.py
index 2b1e488..825dd76 100644
--- a/pyanalyze/test_stacked_scopes.py
+++ b/pyanalyze/test_stacked_scopes.py
@@ -22,13 +22,13 @@ from .value import (
 
 
 # just used for its __dict__
-class Module(object):
+class Module:
     foo = 1
     bar = None
 
 
-class TestStackedScopes(object):
-    def setup(self):
+class TestStackedScopes:
+    def setup_method(self):
         self.scopes = build_stacked_scopes(Module)
 
     def test_scope_type(self):
diff --git a/pyanalyze/test_typeddict.py b/pyanalyze/test_typeddict.py
index b1c1600..68279bf 100644
--- a/pyanalyze/test_typeddict.py
+++ b/pyanalyze/test_typeddict.py
@@ -75,6 +75,7 @@ class TestExtraKeys(TestNameCheckVisitorBase):
     def test_compatibility(self):
         from pyanalyze.extensions import has_extra_keys
         from typing_extensions import TypedDict
+        from typing import Any, Dict
 
         @has_extra_keys(int)
         class TD(TypedDict):
@@ -91,10 +92,11 @@ class TestExtraKeys(TestNameCheckVisitorBase):
         def want_td(td: TD) -> None:
             pass
 
-        def capybara(td: TD, td2: TD2, td3: TD3) -> None:
+        def capybara(td: TD, td2: TD2, td3: TD3, anydict: Dict[str, Any]) -> None:
             want_td(td)
             want_td(td2)
             want_td(td3)  # E: incompatible_argument
+            want_td(anydict)
 
     @assert_passes()
     def test_iteration(self):
@@ -111,13 +113,13 @@ class TestExtraKeys(TestNameCheckVisitorBase):
 
         def capybara(td: TD, td2: TD2) -> None:
             for k, v in td.items():
-                assert_type(k, Union[str, Literal["a"]])
+                assert_type(k, str)
                 assert_type(v, Union[int, str])
             for k in td:
                 assert_type(k, Union[str, Literal["a"]])
 
             for k, v in td2.items():
-                assert_type(k, Literal["a"])
+                assert_type(k, str)
                 assert_type(v, str)
             for k in td2:
                 assert_type(k, Literal["a"])
diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py
index ca0b58c..ce3e435 100644
--- a/pyanalyze/test_typeshed.py
+++ b/pyanalyze/test_typeshed.py
@@ -400,7 +400,7 @@ class GenericChild(Parent[T]):
 
 
 class TestGetGenericBases:
-    def setup(self) -> None:
+    def setup_method(self) -> None:
         checker = Checker()
         self.get_generic_bases = checker.arg_spec_cache.get_generic_bases