diff options
author | Jelle Zijlstra <jelle.zijlstra@gmail.com> | 2022-04-05 14:21:03 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-05 14:21:03 (GMT) |
commit | cfb849a326e52a4edc577112ebf60e1d9d0d7fdb (patch) | |
tree | 275214ff4cbeb981b7f579f3c9f953e260ac11a7 | |
parent | a7551247e7cb7010fb4735281f1afa4abeb8a9cc (diff) | |
download | cpython-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.rst | 29 | ||||
-rw-r--r-- | Lib/test/test_typing.py | 56 | ||||
-rw-r--r-- | Lib/typing.py | 31 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2022-03-22-19-18-31.bpo-47088.JM1kNI.rst | 2 |
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. |