[Python-checkins] bpo-47088: Add typing.LiteralString (PEP 675) (GH-32064)

JelleZijlstra webhook-mailer at python.org
Tue Apr 5 10:21:12 EDT 2022


https://github.com/python/cpython/commit/cfb849a326e52a4edc577112ebf60e1d9d0d7fdb
commit: cfb849a326e52a4edc577112ebf60e1d9d0d7fdb
branch: main
author: Jelle Zijlstra <jelle.zijlstra at gmail.com>
committer: JelleZijlstra <jelle.zijlstra at gmail.com>
date: 2022-04-05T07:21:03-07:00
summary:

bpo-47088: Add typing.LiteralString (PEP 675) (GH-32064)

Co-authored-by: Nick Pope <nick at nickpope.me.uk>

files:
A Misc/NEWS.d/next/Library/2022-03-22-19-18-31.bpo-47088.JM1kNI.rst
M Doc/library/typing.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 0a4e848c67736..fdd00a277b507 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -76,6 +76,8 @@ annotations. These include:
      *Introducing* :data:`TypeGuard`
 * :pep:`673`: Self type
     *Introducing* :data:`Self`
+* :pep:`675`: Arbitrary Literal String Type
+    *Introducing* :data:`LiteralString`
 
 .. _type-aliases:
 
@@ -585,6 +587,33 @@ These can be used as types in annotations and do not support ``[]``.
        avoiding type checker errors with classes that can duck type anywhere or
        are highly dynamic.
 
+.. data:: LiteralString
+
+   Special type that includes only literal strings. A string
+   literal is compatible with ``LiteralString``, as is another
+   ``LiteralString``, but an object typed as just ``str`` is not.
+   A string created by composing ``LiteralString``-typed objects
+   is also acceptable as a ``LiteralString``.
+
+   Example::
+
+      def run_query(sql: LiteralString) -> ...
+          ...
+
+      def caller(arbitrary_string: str, literal_string: LiteralString) -> None:
+          run_query("SELECT * FROM students")  # ok
+          run_query(literal_string)  # ok
+          run_query("SELECT * FROM " + literal_string)  # ok
+          run_query(arbitrary_string)  # type checker error
+          run_query(  # type checker error
+              f"SELECT * FROM students WHERE name = {arbitrary_string}"
+          )
+
+   This is useful for sensitive APIs where arbitrary user-generated
+   strings could generate problems. For example, the two cases above
+   that generate type checker errors could be vulnerable to an SQL
+   injection attack.
+
 .. data:: Never
 
    The `bottom type <https://en.wikipedia.org/wiki/Bottom_type>`_,
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 041b6ad9ed6dd..e09f8aa3fb849 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -27,7 +27,7 @@
 from typing import IO, TextIO, BinaryIO
 from typing import Pattern, Match
 from typing import Annotated, ForwardRef
-from typing import Self
+from typing import Self, LiteralString
 from typing import TypeAlias
 from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
 from typing import TypeGuard
@@ -265,6 +265,60 @@ def test_alias(self):
         self.assertEqual(get_args(alias_3), (Self,))
 
 
+class LiteralStringTests(BaseTestCase):
+    def test_equality(self):
+        self.assertEqual(LiteralString, LiteralString)
+        self.assertIs(LiteralString, LiteralString)
+        self.assertNotEqual(LiteralString, None)
+
+    def test_basics(self):
+        class Foo:
+            def bar(self) -> LiteralString: ...
+        class FooStr:
+            def bar(self) -> 'LiteralString': ...
+        class FooStrTyping:
+            def bar(self) -> 'typing.LiteralString': ...
+
+        for target in [Foo, FooStr, FooStrTyping]:
+            with self.subTest(target=target):
+                self.assertEqual(gth(target.bar), {'return': LiteralString})
+        self.assertIs(get_origin(LiteralString), None)
+
+    def test_repr(self):
+        self.assertEqual(repr(LiteralString), 'typing.LiteralString')
+
+    def test_cannot_subscript(self):
+        with self.assertRaises(TypeError):
+            LiteralString[int]
+
+    def test_cannot_subclass(self):
+        with self.assertRaises(TypeError):
+            class C(type(LiteralString)):
+                pass
+        with self.assertRaises(TypeError):
+            class C(LiteralString):
+                pass
+
+    def test_cannot_init(self):
+        with self.assertRaises(TypeError):
+            LiteralString()
+        with self.assertRaises(TypeError):
+            type(LiteralString)()
+
+    def test_no_isinstance(self):
+        with self.assertRaises(TypeError):
+            isinstance(1, LiteralString)
+        with self.assertRaises(TypeError):
+            issubclass(int, LiteralString)
+
+    def test_alias(self):
+        alias_1 = Tuple[LiteralString, LiteralString]
+        alias_2 = List[LiteralString]
+        alias_3 = ClassVar[LiteralString]
+        self.assertEqual(get_args(alias_1), (LiteralString, LiteralString))
+        self.assertEqual(get_args(alias_2), (LiteralString,))
+        self.assertEqual(get_args(alias_3), (LiteralString,))
+
 class TypeVarTests(BaseTestCase):
     def test_basic_plain(self):
         T = TypeVar('T')
diff --git a/Lib/typing.py b/Lib/typing.py
index 4636798bd6956..26c6b8c278b73 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -126,6 +126,7 @@ def _idfunc(_, x):
     'get_origin',
     'get_type_hints',
     'is_typeddict',
+    'LiteralString',
     'Never',
     'NewType',
     'no_type_check',
@@ -180,7 +181,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=
     if (isinstance(arg, _GenericAlias) and
             arg.__origin__ in invalid_generic_forms):
         raise TypeError(f"{arg} is not valid as type argument")
-    if arg in (Any, NoReturn, Never, Self, TypeAlias):
+    if arg in (Any, LiteralString, NoReturn, Never, Self, TypeAlias):
         return arg
     if allow_special_forms and arg in (ClassVar, Final):
         return arg
@@ -523,6 +524,34 @@ def returns_self(self) -> Self:
     raise TypeError(f"{self} is not subscriptable")
 
 
+ at _SpecialForm
+def LiteralString(self, parameters):
+    """Represents an arbitrary literal string.
+
+    Example::
+
+        from typing import LiteralString
+
+        def run_query(sql: LiteralString) -> ...
+            ...
+
+        def caller(arbitrary_string: str, literal_string: LiteralString) -> None:
+            run_query("SELECT * FROM students")  # ok
+            run_query(literal_string)  # ok
+            run_query("SELECT * FROM " + literal_string)  # ok
+            run_query(arbitrary_string)  # type checker error
+            run_query(  # type checker error
+                f"SELECT * FROM students WHERE name = {arbitrary_string}"
+            )
+
+    Only string literals and other LiteralStrings are compatible
+    with LiteralString. This provides a tool to help prevent
+    security issues such as SQL injection.
+
+    """
+    raise TypeError(f"{self} is not subscriptable")
+
+
 @_SpecialForm
 def ClassVar(self, parameters):
     """Special type construct to mark class variables.
diff --git a/Misc/NEWS.d/next/Library/2022-03-22-19-18-31.bpo-47088.JM1kNI.rst b/Misc/NEWS.d/next/Library/2022-03-22-19-18-31.bpo-47088.JM1kNI.rst
new file mode 100644
index 0000000000000..10a814e018245
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-03-22-19-18-31.bpo-47088.JM1kNI.rst
@@ -0,0 +1,2 @@
+Implement :data:`typing.LiteralString`, part of :pep:`675`. Patch by Jelle
+Zijlstra.



More information about the Python-checkins mailing list