bpo-47088: Add typing.LiteralString (PEP 675) (GH-32064)
Co-authored-by: Nick Pope <nick@nickpope.me.uk>
This commit is contained in:
parent
a7551247e7
commit
cfb849a326
@ -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>`_,
|
||||
|
@ -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')
|
||||
|
@ -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.
|
||||
|
@ -0,0 +1,2 @@
|
||||
Implement :data:`typing.LiteralString`, part of :pep:`675`. Patch by Jelle
|
||||
Zijlstra.
|
Loading…
x
Reference in New Issue
Block a user