Repository: quora/pyanalyze
Pull request #433 (2022-01-23 01:41:58)

make reveal_type() behave like typing.reveal_type() will


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 726879a..44a021d 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,9 @@
 
 ## Unreleased
 
+- `reveal_type()` and `dump_value()` now return their argument,
+  the anticipated behavior for `typing.reveal_type()` in Python
+  3.11 (#432)
 - Fix return type of async generator functions (#431)
 - Type check function decorators (#428)
 - Handle `NoReturn` in `async def` functions (#427)
diff --git a/pyanalyze/extensions.py b/pyanalyze/extensions.py
index 85da5f4..d029151 100644
--- a/pyanalyze/extensions.py
+++ b/pyanalyze/extensions.py
@@ -368,7 +368,10 @@ class ExternalType(metaclass=_ExternalTypeMeta):
         raise NotImplementedError("just here to fool typing._type_check")
 
 
-def reveal_type(value: object) -> None:
+_T = TypeVar("_T")
+
+
+def reveal_type(value: _T) -> _T:
     """Inspect the inferred type of an expression.
 
     Calling this function will make pyanalyze print out the argument's
@@ -383,8 +386,10 @@ def reveal_type(value: object) -> None:
         def f(x: int) -> None:
             reveal_type(x)  # Revealed type is "int"
 
+    At runtime this returns the argument unchanged.
+
     """
-    pass
+    return value
 
 
 _overloads: Dict[str, List[Callable[..., Any]]] = defaultdict(list)
diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py
index 183b726..0c5917c 100644
--- a/pyanalyze/implementation.py
+++ b/pyanalyze/implementation.py
@@ -1,3 +1,5 @@
+import typing
+import typing_extensions
 from .annotations import type_from_value
 from .error_code import ErrorCode
 from .extensions import reveal_type
@@ -938,23 +940,23 @@ def _assert_is_value_impl(ctx: CallContext) -> Value:
 
 
 def _reveal_type_impl(ctx: CallContext) -> Value:
+    value = ctx.vars["value"]
     if ctx.visitor._is_checking():
-        value = ctx.vars["value"]
         message = f"Revealed type is {ctx.visitor.display_value(value)}"
         ctx.show_error(message, ErrorCode.inference_failure, arg="value")
-    return KnownValue(None)
+    return value
 
 
 def _dump_value_impl(ctx: CallContext) -> Value:
+    value = ctx.vars["value"]
     if ctx.visitor._is_checking():
-        value = ctx.vars["value"]
         message = f"Value is '{value!r}'"
         if isinstance(value, KnownValue):
             sig = ctx.visitor.arg_spec_cache.get_argspec(value.val)
             if sig is not None:
                 message += f", signature is {sig!r}"
         ctx.show_error(message, ErrorCode.inference_failure, arg="value")
-    return KnownValue(None)
+    return value
 
 
 def _str_format_impl(ctx: CallContext) -> Value:
@@ -1104,6 +1106,7 @@ _ENCODING_PARAMETER = SigParameter(
     "encoding", annotation=TypedValue(str), default=KnownValue("")
 )
 
+T = TypeVar("T")
 K = TypeVar("K")
 V = TypeVar("V")
 
@@ -1132,14 +1135,14 @@ def get_default_argspecs() -> Dict[object, Signature]:
             callable=assert_is_value,
         ),
         Signature.make(
-            [SigParameter("value", _POS_ONLY)],
-            KnownValue(None),
+            [SigParameter("value", _POS_ONLY, annotation=TypeVarValue(T))],
+            TypeVarValue(T),
             impl=_reveal_type_impl,
             callable=reveal_type,
         ),
         Signature.make(
-            [SigParameter("value", _POS_ONLY)],
-            KnownValue(None),
+            [SigParameter("value", _POS_ONLY, annotation=TypeVarValue(T))],
+            TypeVarValue(T),
             impl=_dump_value_impl,
             callable=dump_value,
         ),
@@ -1549,4 +1552,18 @@ def get_default_argspecs() -> Dict[object, Signature]:
             ),
         ),
     ]
+    for mod in typing, typing_extensions:
+        try:
+            reveal_type_func = getattr(mod, "reveal_type")
+        except AttributeError:
+            pass
+        else:
+            # Anticipating https://bugs.python.org/issue46414
+            sig = Signature.make(
+                [SigParameter("value", _POS_ONLY, annotation=TypeVarValue(T))],
+                TypeVarValue(T),
+                impl=_reveal_type_impl,
+                callable=reveal_type_func,
+            )
+            signatures.append(sig)
     return {sig.callable: sig for sig in signatures}
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index ab24991..3504a50 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -301,16 +301,18 @@ def assert_is_value(obj: object, value: Value, *, skip_annotated: bool = False)
     pass
 
 
-def dump_value(value: object) -> None:
+def dump_value(value: T) -> T:
     """Print out the :class:`Value` representation of its argument.
 
     Calling it will make pyanalyze print out an internal
-    representation of the argument's inferred value. Does nothing
-    at runtime. Use :func:`pyanalyze.extensions.reveal_type` for a
+    representation of the argument's inferred value. Use
+    :func:`pyanalyze.extensions.reveal_type` for a
     more user-friendly representation.
 
+    At runtime this returns the argument unchanged.
+
     """
-    pass
+    return value
 
 
 class AnySource(enum.Enum):


Test patch:
diff --git a/pyanalyze/test_implementation.py b/pyanalyze/test_implementation.py
index 619ce32..4b3dd3a 100644
--- a/pyanalyze/test_implementation.py
+++ b/pyanalyze/test_implementation.py
@@ -1174,6 +1174,16 @@ class TestInferenceHelpers(TestNameCheckVisitorBase):
             assert_is_value(1, KnownValue(2))  # E: inference_failure
             assert_is_value(1, val)  # E: inference_failure
 
+    @assert_passes()
+    def test_return_value(self) -> None:
+        from pyanalyze import dump_value, assert_is_value
+
+        def capybara():
+            x = dump_value(1)  # E: inference_failure
+            y = reveal_type(1)  # E: inference_failure
+            assert_is_value(x, KnownValue(1))
+            assert_is_value(y, KnownValue(1))
+
 
 class TestCallableGuards(TestNameCheckVisitorBase):
     @assert_passes()