Repository: quora/pyanalyze
Pull request #674 (2023-09-10 04:39:27)

Add pyanalyze.runtime


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 50ba9d6..12a9e23 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Expose a convenience API for runtime type checking in the
+  `pyanalyze.runtime` module (#674)
 - Support for annotations from the `annotated-types` library (#673)
 - Detect undefined attributes on Pydantic models (#670)
 - Remove duplicate "attribute_is_never_set" error for classes
diff --git a/docs/index.rst b/docs/index.rst
index 4af9059..82281b1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -21,6 +21,7 @@ pyanalyze: A semi-static typechecker
    reference/ast_annotator
    reference/extensions
    reference/name_check_visitor
+   reference/runtime
    reference/signature
    reference/stacked_scopes
    reference/value
diff --git a/docs/reference/runtime.rst b/docs/reference/runtime.rst
new file mode 100644
index 0000000..2ac0c4b
--- /dev/null
+++ b/docs/reference/runtime.rst
@@ -0,0 +1,5 @@
+pyanalyze.runtime
+=================
+
+.. automodule:: pyanalyze.runtime
+   :members:
diff --git a/pyanalyze/__init__.py b/pyanalyze/__init__.py
index a750ccc..b40d599 100644
--- a/pyanalyze/__init__.py
+++ b/pyanalyze/__init__.py
@@ -27,6 +27,7 @@ from . import (
     predicates,
     reexport,
     safe,
+    runtime,
     shared_options,
     signature,
     stacked_scopes,
@@ -66,3 +67,4 @@ used(functions)
 used(predicates)
 used(typevar)
 used(annotated_types)
+used(runtime)
diff --git a/pyanalyze/extensions.py b/pyanalyze/extensions.py
index cb03e09..b0034cc 100644
--- a/pyanalyze/extensions.py
+++ b/pyanalyze/extensions.py
@@ -98,7 +98,7 @@ class CustomCheck:
 class LiteralOnly(CustomCheck):
     """Custom check that allows only values pyanalyze infers as literals.
 
-    Example:
+    Example::
 
         def func(arg: Annotated[str, LiteralOnly()]) -> None:
             ...
diff --git a/pyanalyze/runtime.py b/pyanalyze/runtime.py
new file mode 100644
index 0000000..052395c
--- /dev/null
+++ b/pyanalyze/runtime.py
@@ -0,0 +1,56 @@
+"""
+
+Expose an interface for a runtime type checker.
+
+"""
+
+from typing import Optional
+from pyanalyze.value import CanAssignError, KnownValue
+from .annotations import type_from_runtime
+from .checker import Checker
+from .find_unused import used
+
+_CHECKER = Checker()
+
+
+@used
+def is_compatible(typ: object, value: object) -> bool:
+    """Return whether ``value`` is compatible with ``type``.
+
+    Examples::
+
+        >>> is_compatible(list[int], 42)
+        False
+        >>> is_compatible(list[int], [])
+        True
+        >>> is_compatible(list[int], ["x"])
+        False
+
+    """
+    val = type_from_runtime(typ)
+    can_assign = val.can_assign(KnownValue(value), _CHECKER)
+    return not isinstance(can_assign, CanAssignError)
+
+
+@used
+def get_compatibility_error(typ: object, value: object) -> Optional[str]:
+    """Return an error message explaining why ``value`` is not
+    compatible with ``type``, or None if they are compatible.
+
+    Examples::
+
+        >>> print(get_compatibility_error(list[int], 42))
+        Cannot assign Literal[42] to list
+
+        >>> print(get_compatibility_error(list[int], []))
+        None
+        >>> print(get_compatibility_error(list[int], ["x"]))
+        In element 0
+          Cannot assign Literal['x'] to int
+
+    """
+    val = type_from_runtime(typ)
+    can_assign = val.can_assign(KnownValue(value), _CHECKER)
+    if isinstance(can_assign, CanAssignError):
+        return can_assign.display(depth=0)
+    return None


Test patch:
diff --git a/pyanalyze/test_runtime.py b/pyanalyze/test_runtime.py
new file mode 100644
index 0000000..6741ae7
--- /dev/null
+++ b/pyanalyze/test_runtime.py
@@ -0,0 +1,22 @@
+from typing import List
+
+from .runtime import is_compatible, get_compatibility_error
+
+
+def test_is_compatible() -> None:
+    assert not is_compatible(List[int], 42)
+    assert is_compatible(List[int], [])
+    assert not is_compatible(List[int], ["x"])
+    assert is_compatible(List[int], [1, 2, 3])
+
+
+def test_get_compatibility_error() -> None:
+    assert (
+        get_compatibility_error(List[int], 42) == "Cannot assign Literal[42] to list\n"
+    )
+    assert get_compatibility_error(List[int], []) is None
+    assert (
+        get_compatibility_error(List[int], ["x"])
+        == "In element 0\n  Cannot assign Literal['x'] to int\n"
+    )
+    assert get_compatibility_error(List[int], [1, 2, 3]) is None