Repository: quora/pyanalyze
Pull request #514 (2022-04-09 02:38:05)

Add support for LiteralString


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 7dcec0d..0a3d696 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Support `LiteralString` (PEP 675) (#514)
 - Add `unused_assignment` error code, separated out from
   `unused_variable`. Enable these error codes and
   `possibly_undefined_name` by default (#511)
diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py
index 8cd12ab..f10bce4 100644
--- a/pyanalyze/annotations.py
+++ b/pyanalyze/annotations.py
@@ -450,6 +450,8 @@ def _type_from_runtime(val: Any, ctx: Context, is_typeddict: bool = False) -> Va
         return NO_RETURN_VALUE
     elif is_typing_name(val, "Self"):
         return SelfTVV
+    elif is_typing_name(val, "LiteralString"):
+        return TypedValue(str, literal_only=True)
     elif hasattr(val, "__supertype__"):
         if isinstance(val.__supertype__, type):
             # NewType
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index d4fb5b3..e19b997 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -600,6 +600,8 @@ class TypedValue(Value):
 
     typ: Union[type, str]
     """The underlying type, or a fully qualified reference to one."""
+    literal_only: bool = False
+    """True if this is LiteralString (PEP 675)."""
     _type_object: Optional["pyanalyze.type_object.TypeObject"] = field(
         init=False, repr=False, hash=False, compare=False, default=None
     )
@@ -627,6 +629,8 @@ class TypedValue(Value):
                     return {}
             return can_assign
         elif isinstance(other, TypedValue):
+            if self.literal_only and not other.literal_only:
+                return CanAssignError(f"{other} is not a literal")
             return self_tobj.can_assign(self, other, ctx)
         elif isinstance(other, SubclassValue):
             if isinstance(other.typ, TypedValue):
@@ -709,9 +713,15 @@ class TypedValue(Value):
         return KnownValue(self.typ)
 
     def __str__(self) -> str:
+        if self.literal_only:
+            if self.typ is str:
+                return "LiteralString"
+            suffix = " (literal only)"
+        else:
+            suffix = ""
         if self._type_object is not None:
-            return str(self._type_object)
-        return stringify_object(self.typ)
+            return f"{self._type_object}{suffix}"
+        return stringify_object(self.typ) + suffix
 
 
 @dataclass(unsafe_hash=True, init=False)


Test patch:
diff --git a/pyanalyze/test_literal_string.py b/pyanalyze/test_literal_string.py
new file mode 100644
index 0000000..c83c898
--- /dev/null
+++ b/pyanalyze/test_literal_string.py
@@ -0,0 +1,20 @@
+# static analysis: ignore
+from .value import TypedValue
+from .implementation import assert_is_value
+from .test_node_visitor import assert_passes
+from .test_name_check_visitor import TestNameCheckVisitorBase
+
+
+class TestLiteralString(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test(self):
+        from typing_extensions import LiteralString
+
+        def f(x: LiteralString) -> LiteralString:
+            return "x"
+
+        def capybara(x: str, y: LiteralString):
+            f(x)  # E: incompatible_argument
+            f(y)
+            f("x")
+            assert_is_value(f("x"), TypedValue(str, literal_only=True))
diff --git a/pyanalyze/test_value.py b/pyanalyze/test_value.py
index ff188e5..82bfb09 100644
--- a/pyanalyze/test_value.py
+++ b/pyanalyze/test_value.py
@@ -117,8 +117,8 @@ def test_unbound_method_value() -> None:
 
 def test_typed_value() -> None:
     val = TypedValue(str)
-    assert str is val.typ
-    assert "str" == str(val)
+    assert val.typ is str
+    assert str(val) == "str"
     assert val.is_type(str)
     assert not val.is_type(int)
 
@@ -128,8 +128,15 @@ def test_typed_value() -> None:
     assert_can_assign(val, MultiValuedValue([val, KnownValue("x")]))
     assert_cannot_assign(val, MultiValuedValue([KnownValue("x"), TypedValue(int)]))
 
+    literal_string = TypedValue(str, literal_only=True)
+    assert literal_string.typ is str
+    assert str(literal_string) == "LiteralString"
+    assert_can_assign(val, literal_string)
+    assert_cannot_assign(literal_string, val)
+    assert_can_assign(literal_string, KnownValue("x"))
+
     float_val = TypedValue(float)
-    assert "float" == str(float_val)
+    assert str(float_val) == "float"
     assert_can_assign(float_val, KnownValue(1.0))
     assert_can_assign(float_val, KnownValue(1))
     assert_cannot_assign(float_val, KnownValue(""))