Repository: quora/pyanalyze
Pull request #785 (2024-07-03 23:53:04)

Rename is_compatible to is_assignable


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 8be3087..3f2c1d8 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,9 @@
 
 ## Unreleased
 
+- Rename the `is_compatible` and `get_compatibility_error` functions
+  to `is_assignable` and `get_assignability_error` to align with the
+  terminology in the typing spec (#785)
 - Fix binary operations involving unions wrapped in `Annotated` (#779)
 - Fix various issues with Python 3.13 and 3.14 support (#773)
 - Improve `ParamSpec` support (#772, #777)
diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py
index a77fa35..3676aa4 100644
--- a/pyanalyze/implementation.py
+++ b/pyanalyze/implementation.py
@@ -20,13 +20,13 @@ from typing import (
 import qcore
 import typing_extensions
 
+from . import runtime
 from .annotated_types import MaxLen, MinLen
 from .annotations import type_from_value
 from .error_code import ErrorCode
 from .extensions import assert_type, reveal_locals, reveal_type
 from .format_strings import parse_format_string
 from .predicates import IsAssignablePredicate
-from .runtime import is_compatible
 from .safe import hasattr_static, is_union, safe_isinstance, safe_issubclass
 from .signature import (
     ANY_SIGNATURE,
@@ -1342,7 +1342,7 @@ def _assert_is_value_impl(ctx: CallContext) -> Value:
     return KnownValue(None)
 
 
-def _is_compatible_impl(ctx: CallContext) -> Value:
+def _is_assignable_impl(ctx: CallContext) -> Value:
     typ = ctx.vars["typ"]
     if not isinstance(typ, KnownValue):
         return TypedValue(bool)
@@ -1697,8 +1697,14 @@ def get_default_argspecs() -> Dict[object, Signature]:
         Signature.make(
             [SigParameter("value"), SigParameter("typ")],
             return_annotation=TypedValue(bool),
-            impl=_is_compatible_impl,
-            callable=is_compatible,
+            impl=_is_assignable_impl,
+            callable=runtime.is_assignable,
+        ),
+        Signature.make(
+            [SigParameter("value"), SigParameter("typ")],
+            return_annotation=TypedValue(bool),
+            impl=_is_assignable_impl,
+            callable=runtime.is_compatible,  # static analysis: ignore[deprecated]
         ),
         Signature.make(
             [SigParameter("value", _POS_ONLY, annotation=TypeVarValue(T))],
diff --git a/pyanalyze/runtime.py b/pyanalyze/runtime.py
index 3ef2097..22e5d0c 100644
--- a/pyanalyze/runtime.py
+++ b/pyanalyze/runtime.py
@@ -7,6 +7,8 @@ Expose an interface for a runtime type checker.
 from functools import lru_cache
 from typing import Optional
 
+from typing_extensions import deprecated
+
 import pyanalyze
 
 from .annotations import type_from_runtime
@@ -19,19 +21,23 @@ def _get_checker() -> "pyanalyze.checker.Checker":
     return pyanalyze.checker.Checker()
 
 
-@used
-def is_compatible(value: object, typ: object) -> bool:
-    """Return whether ``value`` is compatible with ``type``.
+def is_assignable(value: object, typ: object) -> bool:
+    """Return whether ``value`` is assignable to ``typ``.
 
+    This is essentially a more powerful version of ``isinstance()``.
     Examples::
 
-        >>> is_compatible(42, list[int])
+        >>> is_assignable(42, list[int])
         False
-        >>> is_compatible([], list[int])
+        >>> is_assignable([], list[int])
         True
-        >>> is_compatible(["x"], list[int])
+        >>> is_assignable(["x"], list[int])
         False
 
+    The term "assignable" is defined in the typing specification:
+
+        https://typing.readthedocs.io/en/latest/spec/glossary.html#term-assignable
+
     """
     val = type_from_runtime(typ)
     can_assign = val.can_assign(KnownValue(value), _get_checker())
@@ -39,18 +45,18 @@ def is_compatible(value: object, typ: object) -> bool:
 
 
 @used
-def get_compatibility_error(value: object, typ: object) -> Optional[str]:
+def get_assignability_error(value: object, typ: object) -> Optional[str]:
     """Return an error message explaining why ``value`` is not
-    compatible with ``type``, or None if they are compatible.
+    assignable to ``type``, or None if it is assignable.
 
     Examples::
 
-        >>> print(get_compatibility_error(42, list[int]))
+        >>> print(get_assignability_error(42, list[int]))
         Cannot assign Literal[42] to list
 
-        >>> print(get_compatibility_error([], list[int]))
+        >>> print(get_assignability_error([], list[int]))
         None
-        >>> print(get_compatibility_error(["x"], list[int]))
+        >>> print(get_assignability_error(["x"], list[int]))
         In element 0
           Cannot assign Literal['x'] to int
 
@@ -60,3 +66,17 @@ def get_compatibility_error(value: object, typ: object) -> Optional[str]:
     if isinstance(can_assign, CanAssignError):
         return can_assign.display(depth=0)
     return None
+
+
+@used
+@deprecated("Use is_assignable instead")
+def is_compatible(value: object, typ: object) -> bool:
+    """Deprecated alias for is_assignable(). Use that instead."""
+    return is_assignable(value, typ)
+
+
+@used
+@deprecated("Use get_assignability_error instead")
+def get_compatibility_error(value: object, typ: object) -> Optional[str]:
+    """Deprecated alias for get_assignability_error(). Use that instead."""
+    return get_assignability_error(value, typ)


Test patch:
diff --git a/pyanalyze/test_runtime.py b/pyanalyze/test_runtime.py
index 5b0b0c1..8a1f489 100644
--- a/pyanalyze/test_runtime.py
+++ b/pyanalyze/test_runtime.py
@@ -1,28 +1,28 @@
 # static analysis: ignore
 from typing import List
 
-from .runtime import get_compatibility_error, is_compatible
+from .runtime import get_assignability_error, is_assignable
 from .test_name_check_visitor import TestNameCheckVisitorBase
 from .test_node_visitor import assert_passes
 
 
-def test_is_compatible() -> None:
-    assert not is_compatible(42, List[int])
-    assert is_compatible([], List[int])
-    assert not is_compatible(["x"], List[int])
-    assert is_compatible([1, 2, 3], List[int])
+def test_is_assignable() -> None:
+    assert not is_assignable(42, List[int])
+    assert is_assignable([], List[int])
+    assert not is_assignable(["x"], List[int])
+    assert is_assignable([1, 2, 3], List[int])
 
 
-def test_get_compatibility_error() -> None:
+def test_get_assignability_error() -> None:
     assert (
-        get_compatibility_error(42, List[int]) == "Cannot assign Literal[42] to list\n"
+        get_assignability_error(42, List[int]) == "Cannot assign Literal[42] to list\n"
     )
-    assert get_compatibility_error([], List[int]) is None
+    assert get_assignability_error([], List[int]) is None
     assert (
-        get_compatibility_error(["x"], List[int])
+        get_assignability_error(["x"], List[int])
         == "In element 0\n  Cannot assign Literal['x'] to int\n"
     )
-    assert get_compatibility_error([1, 2, 3], List[int]) is None
+    assert get_assignability_error([1, 2, 3], List[int]) is None
 
 
 class TestRuntimeTypeGuard(TestNameCheckVisitorBase):
@@ -31,7 +31,7 @@ class TestRuntimeTypeGuard(TestNameCheckVisitorBase):
         from annotated_types import Predicate
         from typing_extensions import Annotated
 
-        from pyanalyze.runtime import is_compatible
+        from pyanalyze.runtime import is_assignable
 
         IsLower = Annotated[str, Predicate(str.islower)]
 
@@ -40,9 +40,9 @@ class TestRuntimeTypeGuard(TestNameCheckVisitorBase):
 
         def capybara(s: str) -> None:
             want_lowercase(s)  # E: incompatible_argument
-            if is_compatible(s, IsLower):
+            if is_assignable(s, IsLower):
                 want_lowercase(s)
 
         def asserting_capybara(s: str) -> None:
-            assert is_compatible(s, IsLower)
+            assert is_assignable(s, IsLower)
             want_lowercase(s)