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

Co-authored-by: Nick Pope <nick@nickpope.me.uk>
This commit is contained in:
Jelle Zijlstra 2022-04-05 07:21:03 -07:00 committed by GitHub
parent a7551247e7
commit cfb849a326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 116 additions and 2 deletions

View File

@ -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>`_,

View File

@ -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')

View File

@ -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
@ -523,6 +524,34 @@ def Self(self, parameters):
raise TypeError(f"{self} is not subscriptable")
@_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.

View File

@ -0,0 +1,2 @@
Implement :data:`typing.LiteralString`, part of :pep:`675`. Patch by Jelle
Zijlstra.