Repository: quora/pyanalyze
Pull request #378 (2021-12-30 08:49:26)

fix use of TypeVar in default


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index b69b181..7e9a8f8 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Fix usage of type variables in function parameters
+  with defaults (#378)
 - Support the Python 3.10 `match` statement (#376)
 - Support the walrus (`:=`) operator (#375)
 - Initial support for proposed new "type evaluation"
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index 6a0b0d5..dad35f1 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -395,7 +395,7 @@ class Signature:
         - A Value or None, used for union decomposition with overloads.
 
         """
-        if param.annotation != UNANNOTATED and composite.value is not param.default:
+        if param.annotation != UNANNOTATED:
             if typevar_map:
                 param_typ = param.annotation.substitute_typevars(typevar_map)
             else:
@@ -404,18 +404,21 @@ class Signature:
                 param_typ, composite.value, visitor
             )
             if isinstance(tv_map, CanAssignError):
-                if is_overload:
-                    triple = decompose_union(param_typ, composite.value, visitor)
-                    if triple is not None:
-                        return triple
-                visitor.show_error(
-                    composite.node if composite.node is not None else node,
-                    f"Incompatible argument type for {param.name}: expected {param_typ}"
-                    f" but got {composite.value}",
-                    ErrorCode.incompatible_argument,
-                    detail=str(tv_map),
-                )
-                return None, False, None
+                if composite.value is param.default:
+                    tv_map = {}
+                else:
+                    if is_overload:
+                        triple = decompose_union(param_typ, composite.value, visitor)
+                        if triple is not None:
+                            return triple
+                    visitor.show_error(
+                        composite.node if composite.node is not None else node,
+                        f"Incompatible argument type for {param.name}: expected"
+                        f" {param_typ} but got {composite.value}",
+                        ErrorCode.incompatible_argument,
+                        detail=str(tv_map),
+                    )
+                    return None, False, None
             return tv_map, used_any, None
         return {}, False, None
 


Test patch:
diff --git a/pyanalyze/test_arg_spec.py b/pyanalyze/test_arg_spec.py
index 8deef74..74cd697 100644
--- a/pyanalyze/test_arg_spec.py
+++ b/pyanalyze/test_arg_spec.py
@@ -327,7 +327,7 @@ class TestCoroutines(TestNameCheckVisitorBase):
             # annotated as def ... -> Future in typeshed
             assert_is_value(
                 asyncio.sleep(3),
-                GenericValue(asyncio.Future, [AnyValue(AnySource.generic_argument)]),
+                GenericValue(asyncio.Future, [AnyValue(AnySource.unannotated)]),
             )
             return 42
 
diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py
index c33c639..d9454f2 100644
--- a/pyanalyze/test_signature.py
+++ b/pyanalyze/test_signature.py
@@ -954,6 +954,24 @@ class TestTypeVar(TestNameCheckVisitorBase):
             def capybara(self) -> None:
                 super().capybara()
 
+    @assert_passes()
+    def test_default(self):
+        from typing import TypeVar, Dict, Union
+
+        KT = TypeVar("KT")
+        VT = TypeVar("VT")
+        T = TypeVar("T")
+
+        def dictget(d: Dict[KT, VT], key: KT, default: T = None) -> Union[VT, T]:
+            try:
+                return d[key]
+            except KeyError:
+                return default
+
+        def capybara(d: Dict[str, str], key: str) -> None:
+            assert_is_value(dictget(d, key), TypedValue(str) | KnownValue(None))
+            assert_is_value(dictget(d, key, 1), TypedValue(str) | KnownValue(1))
+
 
 class TestAllowCall(TestNameCheckVisitorBase):
     @assert_passes()