From f6e827230b397c26a114638fd5cac54517905d4d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 10 Feb 2022 18:21:27 -0800 Subject: [PATCH] add LiteralString (PEP 675) (#1053) Co-authored-by: Nikita Sobolev --- typing_extensions/CHANGELOG | 1 + typing_extensions/README.rst | 1 + .../src/test_typing_extensions.py | 76 ++++++++++++++++++- typing_extensions/src/typing_extensions.py | 51 +++++++++++++ 4 files changed, 127 insertions(+), 2 deletions(-) diff --git a/typing_extensions/CHANGELOG b/typing_extensions/CHANGELOG index 092e04a86..b874ddc84 100644 --- a/typing_extensions/CHANGELOG +++ b/typing_extensions/CHANGELOG @@ -1,5 +1,6 @@ # Release 4.x.x +- Runtime support for PEP 675 and `typing_extensions.LiteralString`. - Add `Never` and `assert_never`. Backport from bpo-46475. - `ParamSpec` args and kwargs are now equal to themselves. Backport from bpo-46676. Patch by Gregory Beauregard (@GBeauregard). diff --git a/typing_extensions/README.rst b/typing_extensions/README.rst index a83ed3c82..4430d6698 100644 --- a/typing_extensions/README.rst +++ b/typing_extensions/README.rst @@ -37,6 +37,7 @@ This module currently contains the following: - Experimental features + - ``LiteralString`` (see PEP 675) - ``@dataclass_transform()`` (see PEP 681) - ``NotRequired`` (see PEP 655) - ``Required`` (see PEP 655) diff --git a/typing_extensions/src/test_typing_extensions.py b/typing_extensions/src/test_typing_extensions.py index 911168810..c4db70eb9 100644 --- a/typing_extensions/src/test_typing_extensions.py +++ b/typing_extensions/src/test_typing_extensions.py @@ -22,7 +22,7 @@ from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload, final, is_typeddict -from typing_extensions import dataclass_transform, reveal_type, Never, assert_never +from typing_extensions import dataclass_transform, reveal_type, Never, assert_never, LiteralString try: from typing_extensions import get_type_hints except ImportError: @@ -111,6 +111,11 @@ def test_cannot_instantiate(self): with self.assertRaises(TypeError): type(self.bottom_type)() + def test_pickle(self): + for proto in range(pickle.HIGHEST_PROTOCOL): + pickled = pickle.dumps(self.bottom_type, protocol=proto) + self.assertIs(self.bottom_type, pickle.loads(pickled)) + class NoReturnTests(BottomTypeTestsMixin, BaseTestCase): bottom_type = NoReturn @@ -1896,7 +1901,8 @@ def test_cannot_check_subclass(self): def test_pickle(self): samples = [typing.Any, typing.Union[int, str], typing.Optional[str], Tuple[int, ...], - typing.Callable[[str], bytes]] + typing.Callable[[str], bytes], + Self, LiteralString, Never] for t in samples: x = Annotated[t, "a"] @@ -2290,6 +2296,67 @@ def test_no_isinstance(self): issubclass(int, TypeGuard) +class LiteralStringTests(BaseTestCase): + def test_basics(self): + class Foo: + def bar(self) -> LiteralString: ... + def baz(self) -> "LiteralString": ... + + self.assertEqual(gth(Foo.bar), {'return': LiteralString}) + self.assertEqual(gth(Foo.baz), {'return': LiteralString}) + + @skipUnless(PEP_560, "Python 3.7+ required") + def test_get_origin(self): + from typing_extensions import get_origin + self.assertIsNone(get_origin(LiteralString)) + + def test_repr(self): + if hasattr(typing, 'LiteralString'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(LiteralString), '{}.LiteralString'.format(mod_name)) + + 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): + StringTuple = Tuple[LiteralString, LiteralString] + class Alias: + def return_tuple(self) -> StringTuple: + return ("foo", "pep" + "675") + + def test_typevar(self): + StrT = TypeVar("StrT", bound=LiteralString) + self.assertIs(StrT.__bound__, LiteralString) + + def test_pickle(self): + for proto in range(pickle.HIGHEST_PROTOCOL): + pickled = pickle.dumps(LiteralString, protocol=proto) + self.assertIs(LiteralString, pickle.loads(pickled)) + + class SelfTests(BaseTestCase): def test_basics(self): class Foo: @@ -2331,6 +2398,11 @@ class Alias: def return_tuple(self) -> TupleSelf: return (self, self) + def test_pickle(self): + for proto in range(pickle.HIGHEST_PROTOCOL): + pickled = pickle.dumps(Self, protocol=proto) + self.assertIs(Self, pickle.loads(pickled)) + class FinalDecoratorTests(BaseTestCase): def test_final_unmodified(self): diff --git a/typing_extensions/src/typing_extensions.py b/typing_extensions/src/typing_extensions.py index b67efd0b1..d0bcc3248 100644 --- a/typing_extensions/src/typing_extensions.py +++ b/typing_extensions/src/typing_extensions.py @@ -44,6 +44,7 @@ def _check_generic(cls, parameters): 'ClassVar', 'Concatenate', 'Final', + 'LiteralString', 'ParamSpec', 'Self', 'Type', @@ -2155,6 +2156,56 @@ def __getitem__(self, parameters): return self._getitem(self, parameters) +if hasattr(typing, "LiteralString"): + LiteralString = typing.LiteralString +elif sys.version_info[:2] >= (3, 7): + @_SpecialForm + def LiteralString(self, params): + """Represents an arbitrary literal string. + + Example:: + + from typing_extensions import LiteralString + + def query(sql: LiteralString) -> ...: + ... + + query("SELECT * FROM table") # ok + query(f"SELECT * FROM {input()}") # not ok + + See PEP 675 for details. + + """ + raise TypeError(f"{self} is not subscriptable") +else: + class _LiteralString(typing._FinalTypingBase, _root=True): + """Represents an arbitrary literal string. + + Example:: + + from typing_extensions import LiteralString + + def query(sql: LiteralString) -> ...: + ... + + query("SELECT * FROM table") # ok + query(f"SELECT * FROM {input()}") # not ok + + See PEP 675 for details. + + """ + + __slots__ = () + + def __instancecheck__(self, obj): + raise TypeError(f"{self} cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError(f"{self} cannot be used with issubclass().") + + LiteralString = _LiteralString(_root=True) + + if hasattr(typing, "Self"): Self = typing.Self elif sys.version_info[:2] >= (3, 7):