summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJelle Zijlstra <jelle.zijlstra@gmail.com>2022-04-05 14:21:03 (GMT)
committerGitHub <noreply@github.com>2022-04-05 14:21:03 (GMT)
commitcfb849a326e52a4edc577112ebf60e1d9d0d7fdb (patch)
tree275214ff4cbeb981b7f579f3c9f953e260ac11a7
parenta7551247e7cb7010fb4735281f1afa4abeb8a9cc (diff)
downloadcpython-cfb849a326e52a4edc577112ebf60e1d9d0d7fdb.zip
cpython-cfb849a326e52a4edc577112ebf60e1d9d0d7fdb.tar.gz
cpython-cfb849a326e52a4edc577112ebf60e1d9d0d7fdb.tar.bz2
bpo-47088: Add typing.LiteralString (PEP 675) (GH-32064)
Co-authored-by: Nick Pope <nick@nickpope.me.uk>
-rw-r--r--Doc/library/typing.rst29
-rw-r--r--Lib/test/test_typing.py56
-rw-r--r--Lib/typing.py31
-rw-r--r--Misc/NEWS.d/next/Library/2022-03-22-19-18-31.bpo-47088.JM1kNI.rst2
4 files changed, 116 insertions, 2 deletions
diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 0a4e848..fdd00a2 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 041b6ad..e09f8aa 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -27,7 +27,7 @@ from typing import NamedTuple, TypedDict
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 @@ class SelfTests(BaseTestCase):
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 4636798..26c6b8c 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -126,6 +126,7 @@ __all__ = [
'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
@@ -524,6 +525,34 @@ def Self(self, parameters):
@_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 0000000..10a814e
--- /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.